Skip to content

Commit 8d90812

Browse files
feat(webvh): implement WebVhAnonCredsRegistry with schema and credential definition support
- Added WebVhAnonCredsRegistry class to handle anon credentials with did:webvh identifiers. - Implemented methods for retrieving schemas, credential definitions, and revocation registry definitions. - Introduced validation and error handling for resource resolution and proof verification. - Created utility functions for multihash encoding and base58 conversion. - Added tests for WebVhAnonCredsRegistry and resource transformation. - Updated package.json to include new dependencies for class-transformer and class-validator. Signed-off-by: Brian Richter <brian@aviary.tech>
1 parent 1d09d4c commit 8d90812

File tree

9 files changed

+1304
-5
lines changed

9 files changed

+1304
-5
lines changed

packages/webvh/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@
2222
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --config jest.config.js"
2323
},
2424
"dependencies": {
25+
"@credo-ts/anoncreds": "workspace:*",
2526
"@credo-ts/core": "workspace:*",
26-
"didwebvh-ts": "^2.0.0",
27+
"@multiformats/base-x": "^4.0.1",
28+
"class-transformer": "^0.5.1",
29+
"class-validator": "0.14.1",
30+
"didwebvh-ts": "file:/Users/brian/Projects/bcgov/didwebvh-ts",
31+
"json-canonicalize": "^1.0.6",
2732
"tsyringe": "^4.8.0"
2833
},
2934
"devDependencies": {
3035
"rimraf": "^4.4.0",
3136
"typescript": "~4.9.5"
3237
}
33-
}
38+
}

packages/webvh/src/anoncreds/services/WebVhAnonCredsRegistry.ts

Lines changed: 477 additions & 0 deletions
Large diffs are not rendered by default.

packages/webvh/src/anoncreds/services/__tests__/WebVhAnonCredsRegistry.test.ts

Lines changed: 416 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { JsonTransformer } from '@credo-ts/core'
2+
3+
import { WebVhResource } from '../../utils/transform'
4+
5+
import { MockSchemaResource, MockCredDefResource } from './mock-resources'
6+
7+
describe('WebVhTransform', () => {
8+
it('should correctly transform a schema resource', () => {
9+
const resource = JsonTransformer.fromJSON(MockSchemaResource, WebVhResource)
10+
11+
expect(resource).toBeInstanceOf(WebVhResource)
12+
expect(resource['@context']).toEqual(['https://w3id.org/security/data-integrity/v2'])
13+
expect(resource.type).toEqual(['AttestedResource'])
14+
15+
// Type guard to check if content is a schema
16+
if ('attrNames' in resource.content) {
17+
expect(resource.content.name).toBe('Meeting Invitation')
18+
expect(resource.content.version).toBe('1.1')
19+
expect(Array.isArray(resource.content.attrNames)).toBe(true)
20+
expect(resource.content.attrNames).toContain('email')
21+
} else {
22+
fail('Content should be a schema')
23+
}
24+
})
25+
26+
it('should correctly transform a credential definition resource', () => {
27+
const resource = JsonTransformer.fromJSON(MockCredDefResource, WebVhResource)
28+
29+
expect(resource).toBeInstanceOf(WebVhResource)
30+
expect(resource['@context']).toEqual(['https://w3id.org/security/data-integrity/v2'])
31+
expect(resource.type).toEqual(['AttestedResource'])
32+
33+
// Type guard to check if content is a credential definition
34+
if ('schemaId' in resource.content) {
35+
expect(resource.content.type).toBe('CL')
36+
expect(resource.content.tag).toBe('Meeting Invitation')
37+
expect(resource.content.schemaId).toContain('zQmc3ZT6N3s3UhqTcC5kWcWVoHwnkK6dZVBVfkLtYKY8YJm')
38+
} else {
39+
fail('Content should be a credential definition')
40+
}
41+
})
42+
})
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
export const issuerDid =
2+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4'
3+
4+
export const MockSchemaResource = {
5+
'@context': ['https://w3id.org/security/data-integrity/v2'],
6+
type: ['AttestedResource'],
7+
id: 'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4/resources/zQmc3ZT6N3s3UhqTcC5kWcWVoHwnkK6dZVBVfkLtYKY8YJm',
8+
content: {
9+
issuerId:
10+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4',
11+
attrNames: ['group', 'email', 'date'],
12+
name: 'Meeting Invitation',
13+
version: '1.1',
14+
},
15+
metadata: {
16+
resourceId: 'zQmc3ZT6N3s3UhqTcC5kWcWVoHwnkK6dZVBVfkLtYKY8YJm',
17+
resourceType: 'anonCredsSchema',
18+
resourceName: 'Meeting Invitation',
19+
},
20+
proof: {
21+
type: 'DataIntegrityProof',
22+
cryptosuite: 'eddsa-jcs-2022',
23+
proofPurpose: 'assertionMethod',
24+
proofValue: 'z4RCLxRSVeTM4UnZ6vDmDjEX9pbpdUptXuDTy7h8Fij2npReHXmCUzzb5jTEUg1dFtpjH7tiKNJwXztwSktdjaMtX',
25+
verificationMethod:
26+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4#key-01',
27+
},
28+
}
29+
30+
export const MockCredDefResource = {
31+
'@context': ['https://w3id.org/security/data-integrity/v2'],
32+
type: ['AttestedResource'],
33+
id: 'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4/resources/zQmVrh8pxBhaieoJZG8syFUm3axcC928JrE1gaWo9EBVWMM',
34+
content: {
35+
issuerId:
36+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4',
37+
schemaId:
38+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4/resources/zQmc3ZT6N3s3UhqTcC5kWcWVoHwnkK6dZVBVfkLtYKY8YJm',
39+
type: 'CL',
40+
tag: 'Meeting Invitation',
41+
value: {},
42+
},
43+
metadata: {
44+
resourceId: 'zQmVrh8pxBhaieoJZG8syFUm3axcC928JrE1gaWo9EBVWMM',
45+
resourceType: 'anonCredsCredDef',
46+
resourceName: 'Meeting Invitation',
47+
},
48+
proof: {
49+
type: 'DataIntegrityProof',
50+
cryptosuite: 'eddsa-jcs-2022',
51+
proofPurpose: 'assertionMethod',
52+
proofValue: 'z2P3cR46Qt8r3Zc47EHxou6JkU7sTWd8PFZpQwZR7k8MU2Sntt325dQ1u3bn2ZoQUvoCFdNHWYbmsf5FzpqaF1n41',
53+
verificationMethod:
54+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4#key-01',
55+
},
56+
}
57+
58+
export const MockRevRegDefResource = {
59+
'@context': ['https://w3id.org/security/data-integrity/v2'],
60+
type: ['AttestedResource'],
61+
id: 'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4/resources/zQmWnvUkdcwB3nHhAWyAdQ4Vh3WZwtoatK7rhLTVpABGMqt',
62+
content: {
63+
issuerId:
64+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4',
65+
revocDefType: 'CL_ACCUM',
66+
credDefId:
67+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4/resources/zQmVrh8pxBhaieoJZG8syFUm3axcC928JrE1gaWo9EBVWMM',
68+
tag: '0',
69+
value: {
70+
publicKeys: {
71+
accumKey: {
72+
z: '1 0030D63670F45350DA40A365D583E9F63DF8D354A0D413D0E6375A74619B990E 1 0D95E8FDB17C003B423BA2A525D0A0B19D9A60BCE8C661BC6FF508CD87FF1895 1 0D85DFD466C0D8D92C41F8D1F678D6CD86D3D6C3625CE2C91356C5B1E12A4560 1 00755055AE7B11333AAB9D96CB3997DF3A729971ED08FF3980A853BB7E115DAE 1 11DBDA475F2E240B8CC9B874A8ED7C061778C688BB73CB55D51BC8A47495DA4F 1 23815EC20DD84CE61102953818C475B5D4821A80D46CCEB664020CA1A22036A2 1 00FD1DD0A0767A3A3136BDA5FC6D8EC2D463F47E9DE0B2C8D5B16799EF6942B0 1 0896EDD64B8BAB579153B9E4BE73B9DA822C2D60B3D51886CA504DBC63CA8423 1 0C78EE9EAAB9137A33E580E9C9F40AB83670CD6B50F5EA63E5DD6C76617CCB73 1 1BE09416967802C5BF852CEF50E7DBD5EACA4E9F08D4F8E65F6E60A5332FD1AB 1 18C08D5EE58798D211CFF757323E20A5DBDB984CD0B3BCD5BAFF5B119E68F6B7 1 0FB0C9D143962D4A83587269003F125403E8CBAC14252CFA6B501FD577CB4BBF',
73+
},
74+
},
75+
maxCredNum: 100,
76+
tailsLocation: 'https://tails.anoncreds.vc/hash/H9YggRVzCkjeonb7VWnZfTxrfZGTvTn6oFtg8oizcCvt',
77+
tailsHash: 'H9YggRVzCkjeonb7VWnZfTxrfZGTvTn6oFtg8oizcCvt',
78+
},
79+
},
80+
metadata: {
81+
resourceId: 'zQmWnvUkdcwB3nHhAWyAdQ4Vh3WZwtoatK7rhLTVpABGMqt',
82+
resourceType: 'anonCredsRevocRegDef',
83+
resourceName: '0',
84+
},
85+
links: [
86+
{
87+
id: 'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4/resources/zQmZPkNNca2RYyoeGhUoBkSzUeNm3vW4JDQd61c7YyFcqiA',
88+
type: 'anonCredsStatusList',
89+
timestamp: 1742002941,
90+
},
91+
],
92+
proof: {
93+
type: 'DataIntegrityProof',
94+
cryptosuite: 'eddsa-jcs-2022',
95+
proofPurpose: 'assertionMethod',
96+
proofValue: 'z61CdZfYZbgwvNXV5KWFdTmoMyih9yJsmjdTb28JdY9qCiXZJ3t2cRUJCKaEV3Ummr3RrXiZGfDuFb1548GbU8iTU',
97+
verificationMethod:
98+
'did:webvh:QmRxso8yoATm66gKhp3AKbPSH6ys4XcNVgKT786M99JRpN:id.anoncreds.vc:demo:863862bf-cd3b-44e3-89d4-0a2d7f5cc8d4#key-01',
99+
},
100+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import base from '@multiformats/base-x'
2+
3+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
4+
5+
const base58Converter = base(BASE58_ALPHABET)
6+
7+
export function decodeFromBase58(base58: string) {
8+
return base58Converter.decode(base58)
9+
}
10+
11+
export function encodeToBase58(buffer: Uint8Array) {
12+
return base58Converter.encode(buffer)
13+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { encodeToBase58, decodeFromBase58 } from './base58'
2+
3+
/**
4+
* Note: When hashing JSON objects, you should first canonicalize the JSON using
5+
* the json-canonicalize package to ensure consistent hash generation:
6+
*
7+
* ```
8+
* import { canonicalize } from 'json-canonicalize'
9+
* const jsonString = canonicalize(jsonObject)
10+
* const digestBuffer = createHash('sha256').update(jsonString).digest()
11+
* const multiHash = encodeMultihash(digestBuffer)
12+
* ```
13+
*/
14+
15+
// Hash algorithm codes (from multiformats table)
16+
const HASH_CODES: Record<string, number> = {
17+
sha256: 0x12,
18+
sha512: 0x13,
19+
}
20+
21+
/**
22+
* Creates a multihash from a digest
23+
* @param digest The raw hash digest bytes
24+
* @param algorithm The hash algorithm used (defaults to sha256)
25+
* @returns A multihash encoded buffer
26+
*/
27+
export function createMultihash(digest: Buffer, algorithm = 'sha256'): Buffer {
28+
// Get the code for the hash algorithm
29+
const hashCode = HASH_CODES[algorithm]
30+
if (hashCode === undefined) {
31+
throw new Error(`Unsupported hash algorithm: ${algorithm}`)
32+
}
33+
34+
// Length of the digest in bytes
35+
const digestLength = digest.length
36+
37+
// Create a buffer for the multihash:
38+
// <hash-code><digest-length><digest>
39+
const multihashBuffer = Buffer.alloc(2 + digestLength)
40+
41+
// Write hash code as varint (assuming it fits in a single byte for simplicity)
42+
multihashBuffer[0] = hashCode
43+
44+
// Write length as varint (assuming it fits in a single byte for simplicity)
45+
multihashBuffer[1] = digestLength
46+
47+
// Copy the digest into the buffer
48+
digest.copy(multihashBuffer, 2)
49+
50+
return multihashBuffer
51+
}
52+
53+
/**
54+
* Encodes a digest as a multihash string in base58 format
55+
* @param digest The raw hash digest bytes
56+
* @param algorithm The hash algorithm used (default sha256)
57+
* @returns A base58-encoded multihash string
58+
*/
59+
export function encodeMultihash(digest: Buffer, algorithm = 'sha256'): string {
60+
const multihashBuffer = createMultihash(digest, algorithm)
61+
return `z${encodeToBase58(multihashBuffer)}`
62+
}
63+
64+
/**
65+
* Decodes a multihash string to extract the original digest
66+
* @param multihashString Base58-encoded multihash string
67+
* @returns The original digest buffer
68+
*/
69+
export function decodeMultihash(multihashString: string): { algorithm: string, digest: Buffer } {
70+
// Remove the 'z' prefix if present
71+
const base58String = multihashString.startsWith('z') ? multihashString.slice(1) : multihashString
72+
73+
// Decode from base58
74+
const multihashBuffer = Buffer.from(decodeFromBase58(base58String))
75+
76+
// Extract hash code, length, and digest
77+
const hashCode = multihashBuffer[0]
78+
const digestLength = multihashBuffer[1]
79+
const digest = multihashBuffer.slice(2, 2 + digestLength)
80+
81+
// Get the algorithm name from the hash code
82+
const algorithm = Object.keys(HASH_CODES).find(key => HASH_CODES[key] === hashCode) || 'unknown'
83+
84+
return { algorithm, digest }
85+
}

0 commit comments

Comments
 (0)