Skip to content

Commit 5b3ea25

Browse files
committed
Fix request anchor
1 parent 88f8775 commit 5b3ea25

File tree

3 files changed

+130
-10
lines changed

3 files changed

+130
-10
lines changed

packages/sdk/src/wasm/VerifiablePresentationV1/proof.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,15 @@ export async function createFromAnchor(
187187
) {
188188
throw new Error('Unexpected transaction type found for anchor reference');
189189
}
190-
const expectedAnchor = VerifiablePresentationRequestV1.computeAnchor(
190+
191+
const expectedAnchor = VerifiablePresentationRequestV1.computeAnchorHash(
191192
presentationRequest.requestContext,
192193
presentationRequest.credentialStatements
193194
);
194-
if ((new DataBlob(expectedAnchor).toString(), summary.dataRegistered.data)) {
195+
const transactionAnchor = VerifiablePresentationRequestV1.decodeAnchor(
196+
Buffer.from(summary.dataRegistered.data, 'hex')
197+
);
198+
if (Buffer.from(expectedAnchor).toString('hex') !== Buffer.from(transactionAnchor.hash).toString('hex')) {
195199
throw new Error('presentation anchor verification failed.');
196200
}
197201

packages/sdk/src/wasm/VerifiablePresentationV1/request.ts

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Buffer } from 'buffer/index.js';
2+
import _JB from 'json-bigint';
23

34
import { sha256 } from '../../hash.js';
45
import {
@@ -10,13 +11,17 @@ import {
1011
ConcordiumGRPCClient,
1112
NextAccountNonce,
1213
RegisterDataPayload,
14+
cborDecode,
15+
cborEncode,
1316
signTransaction,
1417
} from '../../index.js';
15-
import { DataBlob, TransactionExpiry, TransactionHash } from '../../types/index.js';
16-
import { CredentialStatement } from '../../web3-id/types.js';
18+
import { ContractAddress, DataBlob, TransactionExpiry, TransactionHash } from '../../types/index.js';
19+
import { CredentialStatement, StatementProverQualifier } from '../../web3-id/types.js';
1720
import { GivenContextJSON, givenContextFromJSON, givenContextToJSON } from './internal.js';
1821
import { CredentialContextLabel, GivenContext } from './types.js';
1922

23+
const JSONBig = _JB({ useNativeBigInt: true, alwaysParseAsBig: true });
24+
2025
// NOTE: renamed from ContextInformation in ADR
2126
export type Context = {
2227
type: 'ConcordiumContextInformationV1';
@@ -39,12 +44,43 @@ export function createSimpleContext(nonce: Uint8Array, connectionId: string, con
3944
});
4045
}
4146

42-
export function computeAnchor(context: Context, credentialStatements: CredentialStatement[]): Uint8Array {
47+
export type AnchorData = {
48+
type: 'CCDVRA';
49+
version: number;
50+
hash: Uint8Array;
51+
public?: string;
52+
};
53+
54+
export function createAnchor(
55+
context: Context,
56+
credentialStatements: CredentialStatement[],
57+
publicInfo?: string
58+
): Uint8Array {
59+
const hash = computeAnchorHash(context, credentialStatements);
60+
const data: AnchorData = { type: 'CCDVRA', version: 1, hash, public: publicInfo };
61+
return cborEncode(data);
62+
}
63+
64+
export function computeAnchorHash(context: Context, credentialStatements: CredentialStatement[]): Uint8Array {
4365
// TODO: this is a quick and dirty anchor implementation that needs to be replaced with
4466
// the one from concordium-base when available.
4567
const contextDigest = Buffer.from(JSON.stringify(context));
46-
const statementsDigest = Buffer.from(JSON.stringify(credentialStatements));
47-
return sha256([contextDigest, statementsDigest]);
68+
const statementsDigest = Buffer.from(JSONBig.stringify(credentialStatements));
69+
return Uint8Array.from(sha256([contextDigest, statementsDigest]));
70+
}
71+
72+
export function decodeAnchor(cbor: Uint8Array): AnchorData {
73+
const value = cborDecode(cbor);
74+
if (typeof value !== 'object' || value === null) throw new Error('Expected a cbor encoded object');
75+
// required fields
76+
if (!('type' in value) || value.type !== 'CCDVRA') throw new Error('Expected "type" to be "CCDVRA"');
77+
if (!('version' in value) || typeof value.version !== 'number')
78+
throw new Error('Expected "version" to be a number');
79+
if (!('hash' in value) || !(value.hash instanceof Uint8Array))
80+
throw new Error('Expected "hash" to be a Uint8Array');
81+
// optional fields
82+
if ('public' in value && typeof value.public !== 'string') throw new Error('Expected "public" to be a string');
83+
return value as AnchorData;
4884
}
4985

5086
// TODO: Should match the w3c spec for a verifiable presentation request and the corresponding
@@ -77,9 +113,30 @@ export type Type = VerifiablePresentationRequestV1;
77113

78114
export function fromJSON(json: JSON): VerifiablePresentationRequestV1 {
79115
const requestContext = { ...json.requestContext, given: json.requestContext.given.map(givenContextFromJSON) };
116+
const statements: CredentialStatement[] = json.credentialStatements.map(({ statement, idQualifier }) => {
117+
let mappedQualifier: StatementProverQualifier;
118+
switch (idQualifier.type) {
119+
case 'id':
120+
mappedQualifier = { type: 'id', issuers: idQualifier.issuers.map(Number) };
121+
break;
122+
case 'cred':
123+
mappedQualifier = { type: 'cred', issuers: idQualifier.issuers.map(Number) };
124+
break;
125+
case 'sci':
126+
mappedQualifier = {
127+
type: 'sci',
128+
issuers: idQualifier.issuers.map((c) => ContractAddress.create(c.index, c.subindex)),
129+
};
130+
break;
131+
default:
132+
mappedQualifier = idQualifier;
133+
}
134+
return { statement, idQualifier: mappedQualifier } as CredentialStatement;
135+
});
136+
80137
return new VerifiablePresentationRequestV1(
81138
requestContext,
82-
json.credentialStatements,
139+
statements,
83140
TransactionHash.fromJSON(json.transactionRef)
84141
);
85142
}
@@ -89,10 +146,11 @@ export async function createAndAchor(
89146
sender: AccountAddress.Type,
90147
signer: AccountSigner,
91148
context: Omit<Context, 'type'>,
92-
credentialStatements: CredentialStatement[]
149+
credentialStatements: CredentialStatement[],
150+
anchorPublicInfo?: string
93151
): Promise<VerifiablePresentationRequestV1> {
94152
const requestContext = createContext(context);
95-
const anchor = computeAnchor(requestContext, credentialStatements);
153+
const anchor = createAnchor(requestContext, credentialStatements, anchorPublicInfo);
96154

97155
const nextNonce: NextAccountNonce = await grpc.getNextAccountNonce(sender);
98156
const header: AccountTransactionHeader = {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import _JB from 'json-bigint';
2+
3+
import { AttributeKeyString, ContractAddress, TransactionHash } from '../../../src/pub/types.ts';
4+
import { VerifiablePresentationRequestV1 } from '../../../src/pub/wasm.ts';
5+
import { CredentialStatementBuilder } from '../../../src/pub/web3-id.ts';
6+
7+
const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true });
8+
9+
describe('VerifiablePresentationRequestV1', () => {
10+
it('should perform successful JSON roundtrip', () => {
11+
const context = VerifiablePresentationRequestV1.createSimpleContext(
12+
Uint8Array.from([0, 1, 2, 3]),
13+
'0102'.repeat(16),
14+
'Wine payment'
15+
);
16+
const builder = new CredentialStatementBuilder();
17+
const statement = builder
18+
.forWeb3IdCredentials([ContractAddress.create(2101), ContractAddress.create(1337, 42)], (b) =>
19+
b.addRange('b', 80n, 1237n).addMembership('c', ['aa', 'ff', 'zz'])
20+
)
21+
.forIdentityCredentials([0, 1, 2], (b) => b.revealAttribute(AttributeKeyString.firstName))
22+
.getStatements();
23+
const transactionRef = TransactionHash.fromHexString('01020304'.repeat(8));
24+
const presentationRequest = VerifiablePresentationRequestV1.create(context, statement, transactionRef);
25+
26+
const json = JSONBig.stringify(presentationRequest);
27+
const roundtrip = VerifiablePresentationRequestV1.fromJSON(JSONBig.parse(json));
28+
expect(presentationRequest).toEqual(roundtrip);
29+
});
30+
31+
it('should compute the correct anchor', () => {
32+
const context = VerifiablePresentationRequestV1.createSimpleContext(
33+
Uint8Array.from([0, 1, 2, 3]),
34+
'0102'.repeat(16),
35+
'Wine payment'
36+
);
37+
const builder = new CredentialStatementBuilder();
38+
const statement = builder
39+
.forWeb3IdCredentials([ContractAddress.create(2101), ContractAddress.create(1337, 42)], (b) =>
40+
b.addRange('b', 80n, 1237n).addMembership('c', ['aa', 'ff', 'zz'])
41+
)
42+
.forIdentityCredentials([0, 1, 2], (b) => b.revealAttribute(AttributeKeyString.firstName))
43+
.getStatements();
44+
const anchor = VerifiablePresentationRequestV1.createAnchor(context, statement, 'public info');
45+
const expectedAnchor =
46+
'a4646861736858206a6f644b6d22e1647196c4fae44ffef5be554dc0edcac30518ef9624ab44de4f647479706566434344565241667075626c69636b7075626c696320696e666f6776657273696f6e01';
47+
expect(Buffer.from(anchor).toString('hex')).toEqual(expectedAnchor);
48+
49+
const roundtrip = VerifiablePresentationRequestV1.decodeAnchor(anchor);
50+
const expectedData: VerifiablePresentationRequestV1.AnchorData = {
51+
type: 'CCDVRA',
52+
version: 1,
53+
hash: VerifiablePresentationRequestV1.computeAnchorHash(context, statement),
54+
public: 'public info',
55+
};
56+
expect(roundtrip).toEqual(expectedData);
57+
});
58+
});

0 commit comments

Comments
 (0)