From 924b6b52d03be47aba1fe59c318c269a83d2406e Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 30 Apr 2024 14:37:13 -0700 Subject: [PATCH 01/12] Translate from web5-go --- packages/web5/lib/src/pexv2/pd.dart | 179 ++++++++++++++++++ .../lib/src/pexv2/presentation_exchange.dart | 118 ++++++++++++ packages/web5/pubspec.yaml | 1 + packages/web5/test/pexv2/pd_test.dart | 12 ++ 4 files changed, 310 insertions(+) create mode 100644 packages/web5/lib/src/pexv2/pd.dart create mode 100644 packages/web5/lib/src/pexv2/presentation_exchange.dart create mode 100644 packages/web5/test/pexv2/pd_test.dart diff --git a/packages/web5/lib/src/pexv2/pd.dart b/packages/web5/lib/src/pexv2/pd.dart new file mode 100644 index 0000000..908e329 --- /dev/null +++ b/packages/web5/lib/src/pexv2/pd.dart @@ -0,0 +1,179 @@ +/// PresentationDefinition represents a DIF Presentation Definition defined [here]. +/// Presentation Definitions are objects that articulate what proofs a Verifier requires. +/// +/// [here]: https://identity.foundation/presentation-exchange/#presentation-definition +class PresentationDefinition { + String id; + String? name; + String? purpose; + List inputDescriptors; + + PresentationDefinition({ + required this.id, + this.name, + this.purpose, + required this.inputDescriptors, + }); + + factory PresentationDefinition.fromJson(Map json) => + PresentationDefinition( + id: json['id'], + name: json['name'], + purpose: json['purpose'], + inputDescriptors: List.from( + json['input_descriptors'].map((x) => InputDescriptor.fromJson(x)), + ), + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'purpose': purpose, + 'input_descriptors': + List.from(inputDescriptors.map((x) => x.toJson())), + }; +} + +/// InputDescriptor represents a DIF Input Descriptor defined [here]. +/// Input Descriptors are used to describe the information a Verifier requires of a Holder. +/// +/// [here]: https://identity.foundation/presentation-exchange/#input-descriptor +class InputDescriptor { + String id; + String? name; + String? purpose; + Constraints constraints; + + InputDescriptor({ + required this.id, + this.name, + this.purpose, + required this.constraints, + }); + + factory InputDescriptor.fromJson(Map json) => + InputDescriptor( + id: json['id'], + name: json['name'], + purpose: json['purpose'], + constraints: Constraints.fromJson(json['constraints']), + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'purpose': purpose, + 'constraints': constraints.toJson(), + }; +} + +/// Constraints contains the requirements for a given Input Descriptor. +class Constraints { + List? fields; + + Constraints({this.fields}); + + factory Constraints.fromJson(Map json) => Constraints( + fields: json['fields'] == null + ? null + : List.from(json['fields'].map((x) => Field.fromJson(x))), + ); + + Map toJson() => { + 'fields': fields == null + ? null + : List.from(fields!.map((x) => x.toJson())), + }; +} + +/// Field contains the requirements for a given field within a proof. +class Field { + String? id; + String? name; + List? path; + String? purpose; + Filter? filter; + bool? optional; + Optionality? predicate; + + Field({ + this.id, + this.name, + this.path, + this.purpose, + this.filter, + this.optional, + this.predicate, + }); + + factory Field.fromJson(Map json) => Field( + id: json['id'], + name: json['name'], + path: json['path'] == null ? null : List.from(json['path']), + purpose: json['purpose'], + filter: json['filter'] == null ? null : Filter.fromJson(json['filter']), + optional: json['optional'], + predicate: json['predicate'] == null + ? null + : optionalityValues.map[json['predicate']], + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'path': path, + 'purpose': purpose, + 'filter': filter?.toJson(), + 'optional': optional, + 'predicate': + predicate == null ? null : optionalityValues.reverse[predicate], + }; +} + +enum Optionality { required, preferred } + +final optionalityValues = EnumValues({ + 'preferred': Optionality.preferred, + 'required': Optionality.required, +}); + +/// Filter is a JSON Schema that is applied against the value of a field. +class Filter { + String? type; + String? pattern; + String? constValue; + Filter? contains; + + Filter({ + this.type, + this.pattern, + this.constValue, + this.contains, + }); + + factory Filter.fromJson(Map json) => Filter( + type: json['type'], + pattern: json['pattern'], + constValue: json['const'], + contains: + json['contains'] == null ? null : Filter.fromJson(json['contains']), + ); + + Map toJson() => { + 'type': type, + 'pattern': pattern, + 'const': constValue, + 'contains': contains?.toJson(), + }; +} + +/// Helper class for handling enums in JSON. +// TODO might not need this +class EnumValues { + Map map; + Map reverseMap; + + EnumValues(this.map) : reverseMap = map.map((k, v) => MapEntry(v, k)); + + Map get reverse => reverseMap; +} diff --git a/packages/web5/lib/src/pexv2/presentation_exchange.dart b/packages/web5/lib/src/pexv2/presentation_exchange.dart new file mode 100644 index 0000000..9ece86f --- /dev/null +++ b/packages/web5/lib/src/pexv2/presentation_exchange.dart @@ -0,0 +1,118 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import 'package:json_path/json_path.dart'; +import 'package:web5/src/pexv2/pd.dart'; + +class FieldPath { + List paths; + + FieldPath({required this.paths}); +} + +/// This function selects the Verifiable Credentials (VCs) that satisfy the constraints specified in the Presentation Definition +Future> selectCredentials( + List vcJwts, + PresentationDefinition pd, +) async { + final fieldPaths = {}; + final fieldFilters = {}; + + // Extract the field paths and filters from the input descriptors + for (var inputDescriptor in pd.inputDescriptors) { + if (inputDescriptor.constraints.fields == null) { + continue; + } + + for (var field in inputDescriptor.constraints.fields!) { + final token = generateRandomToken(); + final paths = field.path; + if (paths != null) { + fieldPaths[token] = FieldPath(paths: paths); + } + if (field.filter != null) { + fieldFilters[token] = field.filter!; + } + } + } + + final selectionCandidates = {}; + + // Find vcJwts whose fields match the fieldPaths + for (var vcJwt in vcJwts) { + final decoded = json.decode(vcJwt); // Simulating decoding a JWT + + for (var fieldToken in fieldPaths.entries) { + for (var path in fieldToken.value.paths) { + final jsondata = decoded; + final value = JsonPath(path).read(jsondata).firstOrNull; + + if (value != null) { + selectionCandidates[vcJwt] = value; + break; + } + } + } + } + + final matchingVcJWTs = []; + + // If no field filters are specified in PD, return all the vcJwts that matched the fieldPaths + if (fieldFilters.isEmpty) { + return selectionCandidates.keys.toList(); + } + + // Filter further for vcJwts whose fields match the fieldFilters + for (var entry in selectionCandidates.entries) { + for (var filter in fieldFilters.values) { + if (satisfiesFieldFilter(entry.value, filter)) { + matchingVcJWTs.add(entry.key); + } + } + } + + return matchingVcJWTs; +} + +bool satisfiesFieldFilter(dynamic fieldValue, Filter filter) { + // Check if the field value matches the constant if specified + if (filter.constValue != null) { + return fieldValue.toString() == filter.constValue; + } + + // Type checking and pattern matching + if (filter.type != null || filter.pattern != null) { + switch (filter.type) { + case 'string': + if (filter.pattern != null) { + return RegExp(filter.pattern!).hasMatch(fieldValue.toString()); + } + break; + case 'number': + if (fieldValue is num) { + return true; + } + break; + case 'array': + if (fieldValue is List && filter.contains != null) { + return fieldValue + .any((item) => satisfiesFieldFilter(item, filter.contains!)); + } + break; + default: + return false; + } + } + + return true; +} + +String generateRandomToken() { + final rand = Random.secure(); + final bytes = Uint8List(16); + for (int i = 0; i < 16; i++) { + bytes[i] = rand.nextInt(256); + } + return hex.encode(bytes); +} diff --git a/packages/web5/pubspec.yaml b/packages/web5/pubspec.yaml index 0e0aa34..29416d5 100644 --- a/packages/web5/pubspec.yaml +++ b/packages/web5/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: pointycastle: ^3.7.3 http: ^1.2.0 uuid: ^4.4.0 + json_path: ^0.7.1 dev_dependencies: lints: ^3.0.0 diff --git a/packages/web5/test/pexv2/pd_test.dart b/packages/web5/test/pexv2/pd_test.dart new file mode 100644 index 0000000..03dfaed --- /dev/null +++ b/packages/web5/test/pexv2/pd_test.dart @@ -0,0 +1,12 @@ +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:web5/src/crypto.dart'; + +void main() { + + group('bar', () { + test('foo ', () async { + }); + }); +} From 2906e702e3435cd0aa11bea5d32324463d5b62a4 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Thu, 2 May 2024 23:02:06 -0700 Subject: [PATCH 02/12] Use json schema approach --- packages/web5/lib/src/pexv2/pd.dart | 95 ++++++++++++++ .../lib/src/pexv2/presentation_exchange.dart | 118 ------------------ packages/web5/pubspec.yaml | 1 + .../test/helpers/test_vector_helpers.dart | 21 ++++ packages/web5/test/pexv2/pd_test.dart | 66 +++++++++- web5-spec | 2 +- 6 files changed, 179 insertions(+), 124 deletions(-) delete mode 100644 packages/web5/lib/src/pexv2/presentation_exchange.dart create mode 100644 packages/web5/test/helpers/test_vector_helpers.dart diff --git a/packages/web5/lib/src/pexv2/pd.dart b/packages/web5/lib/src/pexv2/pd.dart index 908e329..37f64c1 100644 --- a/packages/web5/lib/src/pexv2/pd.dart +++ b/packages/web5/lib/src/pexv2/pd.dart @@ -1,3 +1,10 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import 'package:json_path/json_path.dart'; +import 'package:json_schema/json_schema.dart'; + /// PresentationDefinition represents a DIF Presentation Definition defined [here]. /// Presentation Definitions are objects that articulate what proofs a Verifier requires. /// @@ -32,6 +39,27 @@ class PresentationDefinition { 'input_descriptors': List.from(inputDescriptors.map((x) => x.toJson())), }; + + List selectCredentials(List vcJwts) { + final Set matches = {}; + + for (final inputDescriptor in inputDescriptors) { + final matchingVcJwts = inputDescriptor.selectCredentials(vcJwts); + if (matchingVcJwts.isEmpty) { + return []; + } + matches.addAll(matchingVcJwts); + } + + return matches.toList(); + } +} + +class _TokenizedField { + List paths; + String token; + + _TokenizedField({required this.paths, required this.token}); } /// InputDescriptor represents a DIF Input Descriptor defined [here]. @@ -65,6 +93,73 @@ class InputDescriptor { 'purpose': purpose, 'constraints': constraints.toJson(), }; + + String _generateRandomToken() { + final rand = Random.secure(); + final bytes = Uint8List(16); + for (int i = 0; i < 16; i++) { + bytes[i] = rand.nextInt(256); + } + return hex.encode(bytes); + } + + List selectCredentials(List vcJWTs) { + final List answer = []; + final List<_TokenizedField> tokenizedField = []; + final schemaMap = { + '\$schema': 'http://json-schema.org/draft-07/schema#', + 'type': 'object', + 'properties': {}, + 'required': [], + }; + + // Populate JSON schema and generate tokens for each field + for (var field in constraints.fields ?? []) { + final token = _generateRandomToken(); + tokenizedField + .add(_TokenizedField(token: token, paths: field.path ?? [])); + + final properties = schemaMap['properties'] as Map; + + if (field.filter != null) { + properties[token] = field.filter.toJson(); + } else { + final anyType = { + 'type': ['string', 'number', 'boolean', 'object', 'array'], + }; + properties[token] = anyType; + } + final required = schemaMap['required'] as List; + required.add(token); + } + final jsonSchema = JsonSchema.create(schemaMap); + + // Tokenize each vcJwt and validate it against the JSON schema + for (var vcJWT in vcJWTs) { + final decoded = json.decode(vcJWT); + + final selectionCandidate = {}; + + for (var tokenPath in tokenizedField) { + for (var path in tokenPath.paths) { + final value = JsonPath(path) + .read(decoded) + .firstOrNull; // Custom function needed to handle JSON paths. + if (value != null) { + selectionCandidate[tokenPath.token] = value; + break; + } + } + } + + final validationResult = jsonSchema.validate(selectionCandidate); + if (validationResult.isValid) { + answer.add(vcJWT); + } + } + + return answer; + } } /// Constraints contains the requirements for a given Input Descriptor. diff --git a/packages/web5/lib/src/pexv2/presentation_exchange.dart b/packages/web5/lib/src/pexv2/presentation_exchange.dart deleted file mode 100644 index 9ece86f..0000000 --- a/packages/web5/lib/src/pexv2/presentation_exchange.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; -import 'dart:typed_data'; -import 'package:convert/convert.dart'; -import 'package:json_path/json_path.dart'; -import 'package:web5/src/pexv2/pd.dart'; - -class FieldPath { - List paths; - - FieldPath({required this.paths}); -} - -/// This function selects the Verifiable Credentials (VCs) that satisfy the constraints specified in the Presentation Definition -Future> selectCredentials( - List vcJwts, - PresentationDefinition pd, -) async { - final fieldPaths = {}; - final fieldFilters = {}; - - // Extract the field paths and filters from the input descriptors - for (var inputDescriptor in pd.inputDescriptors) { - if (inputDescriptor.constraints.fields == null) { - continue; - } - - for (var field in inputDescriptor.constraints.fields!) { - final token = generateRandomToken(); - final paths = field.path; - if (paths != null) { - fieldPaths[token] = FieldPath(paths: paths); - } - if (field.filter != null) { - fieldFilters[token] = field.filter!; - } - } - } - - final selectionCandidates = {}; - - // Find vcJwts whose fields match the fieldPaths - for (var vcJwt in vcJwts) { - final decoded = json.decode(vcJwt); // Simulating decoding a JWT - - for (var fieldToken in fieldPaths.entries) { - for (var path in fieldToken.value.paths) { - final jsondata = decoded; - final value = JsonPath(path).read(jsondata).firstOrNull; - - if (value != null) { - selectionCandidates[vcJwt] = value; - break; - } - } - } - } - - final matchingVcJWTs = []; - - // If no field filters are specified in PD, return all the vcJwts that matched the fieldPaths - if (fieldFilters.isEmpty) { - return selectionCandidates.keys.toList(); - } - - // Filter further for vcJwts whose fields match the fieldFilters - for (var entry in selectionCandidates.entries) { - for (var filter in fieldFilters.values) { - if (satisfiesFieldFilter(entry.value, filter)) { - matchingVcJWTs.add(entry.key); - } - } - } - - return matchingVcJWTs; -} - -bool satisfiesFieldFilter(dynamic fieldValue, Filter filter) { - // Check if the field value matches the constant if specified - if (filter.constValue != null) { - return fieldValue.toString() == filter.constValue; - } - - // Type checking and pattern matching - if (filter.type != null || filter.pattern != null) { - switch (filter.type) { - case 'string': - if (filter.pattern != null) { - return RegExp(filter.pattern!).hasMatch(fieldValue.toString()); - } - break; - case 'number': - if (fieldValue is num) { - return true; - } - break; - case 'array': - if (fieldValue is List && filter.contains != null) { - return fieldValue - .any((item) => satisfiesFieldFilter(item, filter.contains!)); - } - break; - default: - return false; - } - } - - return true; -} - -String generateRandomToken() { - final rand = Random.secure(); - final bytes = Uint8List(16); - for (int i = 0; i < 16; i++) { - bytes[i] = rand.nextInt(256); - } - return hex.encode(bytes); -} diff --git a/packages/web5/pubspec.yaml b/packages/web5/pubspec.yaml index 29416d5..270889f 100644 --- a/packages/web5/pubspec.yaml +++ b/packages/web5/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: http: ^1.2.0 uuid: ^4.4.0 json_path: ^0.7.1 + json_schema: ^5.1.7 dev_dependencies: lints: ^3.0.0 diff --git a/packages/web5/test/helpers/test_vector_helpers.dart b/packages/web5/test/helpers/test_vector_helpers.dart new file mode 100644 index 0000000..385eb9b --- /dev/null +++ b/packages/web5/test/helpers/test_vector_helpers.dart @@ -0,0 +1,21 @@ +import 'dart:convert'; +import 'dart:io'; + +final thisDir = Directory.current.path; +final vectorDir = '$thisDir/../../web5-spec/test-vectors/'; + +Map getJsonVectors(String vectorSubPath) { + final vectorPath = '$vectorDir/$vectorSubPath'; + final file = File(vectorPath); + + try { + // Read the file as a string + final contents = file.readAsStringSync(); + return json.decode(contents); + + } catch (e) { + // If encountering an error, print it + throw Exception('Failed to load verify test vectors: $e'); + } +} + diff --git a/packages/web5/test/pexv2/pd_test.dart b/packages/web5/test/pexv2/pd_test.dart index 03dfaed..390166d 100644 --- a/packages/web5/test/pexv2/pd_test.dart +++ b/packages/web5/test/pexv2/pd_test.dart @@ -1,12 +1,68 @@ -import 'dart:convert'; - import 'package:test/test.dart'; -import 'package:web5/src/crypto.dart'; +import 'package:web5/src/pexv2/pd.dart'; +import '../helpers/test_vector_helpers.dart'; + +class SelectCredentialTestVector { + String description; + + // Input + PresentationDefinition inputPresentationDefinition; + List inputVcJwts; + + // output + List outputSelectedCredentials; + bool errors; + + SelectCredentialTestVector({ + required this.description, + required this.inputPresentationDefinition, + required this.inputVcJwts, + required this.outputSelectedCredentials, + this.errors = false, + }); + + factory SelectCredentialTestVector.fromJson(Map json) { + return SelectCredentialTestVector( + description: json['description'], + inputPresentationDefinition: PresentationDefinition.fromJson( + json['input']['presentationDefinition'], + ), + inputVcJwts: json['input']['credentialJwts'], + outputSelectedCredentials: json['output']['selectedCredentials'], + errors: json['errors'], + ); + } +} void main() { + group('select credentials', () { + group('vectors', () { + final vectorsJson = + getJsonVectors('presentation_exchange/select_credentials.json'); + final vectors = (vectorsJson['vectors'] as List>) + .map(SelectCredentialTestVector.fromJson); + + for (final vector in vectors) { + test(vector.description, () async { + try { + final matchingVcJwts = vector.inputPresentationDefinition + .selectCredentials(vector.inputVcJwts); + + if (vector.errors == true) { + fail('Expected an error but none was thrown'); + } - group('bar', () { - test('foo ', () async { + expect( + Set.from(matchingVcJwts), + Set.from(vector.outputSelectedCredentials), + ); + } catch (e) { + if (vector.errors == false) { + fail('Expected no error but got: $e'); + } + } + }); + } }); }); } diff --git a/web5-spec b/web5-spec index d0e90b7..3580549 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit d0e90b7803f456e80246ace9f57e5583affa6831 +Subproject commit 35805494018d16bb19194e41fd98184314648315 From 57c5fdd0c39fe6bd15a6d223467d28cb6bb2d69d Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Thu, 2 May 2024 23:25:56 -0700 Subject: [PATCH 03/12] Wrap in setUpAll --- packages/web5/test/pexv2/pd_test.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/web5/test/pexv2/pd_test.dart b/packages/web5/test/pexv2/pd_test.dart index 390166d..6ae61d9 100644 --- a/packages/web5/test/pexv2/pd_test.dart +++ b/packages/web5/test/pexv2/pd_test.dart @@ -1,7 +1,9 @@ import 'package:test/test.dart'; import 'package:web5/src/pexv2/pd.dart'; + import '../helpers/test_vector_helpers.dart'; + class SelectCredentialTestVector { String description; @@ -37,10 +39,14 @@ class SelectCredentialTestVector { void main() { group('select credentials', () { group('vectors', () { - final vectorsJson = - getJsonVectors('presentation_exchange/select_credentials.json'); - final vectors = (vectorsJson['vectors'] as List>) - .map(SelectCredentialTestVector.fromJson); + late List vectors; + + setUpAll(() { + final vectorsJson = getJsonVectors('presentation_exchange/select_credentials.json'); + vectors = (vectorsJson['vectors'] as List>) + .map(SelectCredentialTestVector.fromJson) + .toList(); + }); for (final vector in vectors) { test(vector.description, () async { From 9067d0b78ad3bcc112208d847cb36961451ef8ad Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 3 May 2024 00:10:25 -0700 Subject: [PATCH 04/12] Fixed selectCredential vector tests --- .../test/helpers/test_vector_helpers.dart | 4 +-- packages/web5/test/pexv2/pd_test.dart | 35 +++++++++---------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/packages/web5/test/helpers/test_vector_helpers.dart b/packages/web5/test/helpers/test_vector_helpers.dart index 385eb9b..8b8f27d 100644 --- a/packages/web5/test/helpers/test_vector_helpers.dart +++ b/packages/web5/test/helpers/test_vector_helpers.dart @@ -5,17 +5,15 @@ final thisDir = Directory.current.path; final vectorDir = '$thisDir/../../web5-spec/test-vectors/'; Map getJsonVectors(String vectorSubPath) { - final vectorPath = '$vectorDir/$vectorSubPath'; + final vectorPath = '$vectorDir$vectorSubPath'; final file = File(vectorPath); try { // Read the file as a string final contents = file.readAsStringSync(); return json.decode(contents); - } catch (e) { // If encountering an error, print it throw Exception('Failed to load verify test vectors: $e'); } } - diff --git a/packages/web5/test/pexv2/pd_test.dart b/packages/web5/test/pexv2/pd_test.dart index 6ae61d9..9966073 100644 --- a/packages/web5/test/pexv2/pd_test.dart +++ b/packages/web5/test/pexv2/pd_test.dart @@ -3,35 +3,32 @@ import 'package:web5/src/pexv2/pd.dart'; import '../helpers/test_vector_helpers.dart'; - class SelectCredentialTestVector { String description; - - // Input PresentationDefinition inputPresentationDefinition; List inputVcJwts; - - // output List outputSelectedCredentials; - bool errors; + bool? errors; SelectCredentialTestVector({ required this.description, required this.inputPresentationDefinition, required this.inputVcJwts, required this.outputSelectedCredentials, - this.errors = false, }); factory SelectCredentialTestVector.fromJson(Map json) { + final input = Map.from(json['input']); + final output = Map.from(json['output']); + return SelectCredentialTestVector( description: json['description'], inputPresentationDefinition: PresentationDefinition.fromJson( - json['input']['presentationDefinition'], + Map.from(input['presentationDefinition']), ), - inputVcJwts: json['input']['credentialJwts'], - outputSelectedCredentials: json['output']['selectedCredentials'], - errors: json['errors'], + inputVcJwts: List.from(input['credentialJwts']), + outputSelectedCredentials: + List.from(output['selectedCredentials']), ); } } @@ -39,14 +36,14 @@ class SelectCredentialTestVector { void main() { group('select credentials', () { group('vectors', () { - late List vectors; - - setUpAll(() { - final vectorsJson = getJsonVectors('presentation_exchange/select_credentials.json'); - vectors = (vectorsJson['vectors'] as List>) - .map(SelectCredentialTestVector.fromJson) - .toList(); - }); + final vectorsJson = + getJsonVectors('presentation_exchange/select_credentials.json'); + final vectorsJson2 = + getJsonVectors('presentation_exchange/select_credentials_go.json'); + + final vectors = [...vectorsJson['vectors'], ...vectorsJson2['vectors']] + .map((e) => SelectCredentialTestVector.fromJson(e)) + .toList(); for (final vector in vectors) { test(vector.description, () async { From 4cd974a7f309c0c13e4eb2fe815433642063fede Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 3 May 2024 00:11:32 -0700 Subject: [PATCH 05/12] remove useless comment --- packages/web5/test/crypto/secp256k1_test.dart | 2 -- packages/web5/test/helpers/test_vector_helpers.dart | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/web5/test/crypto/secp256k1_test.dart b/packages/web5/test/crypto/secp256k1_test.dart index 548107a..44d5bf3 100644 --- a/packages/web5/test/crypto/secp256k1_test.dart +++ b/packages/web5/test/crypto/secp256k1_test.dart @@ -72,13 +72,11 @@ void main() { final file = File(vectorPath); late List vectors; try { - // Read the file as a string final contents = file.readAsStringSync(); final jsonVectors = json.decode(contents); vectors = TestVectors.fromJson(jsonVectors).vectors; } catch (e) { - // If encountering an error, print it throw Exception('Failed to load verify test vectors: $e'); } diff --git a/packages/web5/test/helpers/test_vector_helpers.dart b/packages/web5/test/helpers/test_vector_helpers.dart index 8b8f27d..eeb5499 100644 --- a/packages/web5/test/helpers/test_vector_helpers.dart +++ b/packages/web5/test/helpers/test_vector_helpers.dart @@ -9,11 +9,9 @@ Map getJsonVectors(String vectorSubPath) { final file = File(vectorPath); try { - // Read the file as a string final contents = file.readAsStringSync(); return json.decode(contents); } catch (e) { - // If encountering an error, print it throw Exception('Failed to load verify test vectors: $e'); } } From 91a177b713d565dacada2924da41acdf59be737d Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 3 May 2024 11:43:20 -0700 Subject: [PATCH 06/12] Use secp256k1 test vectors properly --- packages/web5/test/crypto/secp256k1_test.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/web5/test/crypto/secp256k1_test.dart b/packages/web5/test/crypto/secp256k1_test.dart index 44d5bf3..0cc2a7e 100644 --- a/packages/web5/test/crypto/secp256k1_test.dart +++ b/packages/web5/test/crypto/secp256k1_test.dart @@ -86,14 +86,20 @@ void main() { Uint8List.fromList(hex.decode(vector.input.signature)); final payload = Uint8List.fromList(hex.decode(vector.input.data)); + // Since some other web5 implementations of this Secp256k1.verify() + // return `false` rather than throwing, we should interpret the + // test vectors as expecting failure when either `errors` is true + // or `output` is false. + final shouldThrow = vector.errors || vector.output == false; + try { await Secp256k1.verify(vector.input.key, payload, signature); - if (vector.errors == true) { + if (shouldThrow) { fail('Expected an error but none was thrown'); } } catch (e) { - if (vector.errors == false) { + if (!shouldThrow) { fail('Expected no error but got: $e'); } } From 2c8a92e2d95048b68cd12e657233dc7d8f648665 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 3 May 2024 20:55:07 -0700 Subject: [PATCH 07/12] Formatting and minor clean up --- packages/web5/lib/src/pexv2/pd.dart | 64 ++++++++------------------- packages/web5/lib/src/vc/vc.dart | 11 +++-- packages/web5/test/pexv2/pd_test.dart | 6 +-- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/packages/web5/lib/src/pexv2/pd.dart b/packages/web5/lib/src/pexv2/pd.dart index 37f64c1..6c9964f 100644 --- a/packages/web5/lib/src/pexv2/pd.dart +++ b/packages/web5/lib/src/pexv2/pd.dart @@ -1,14 +1,14 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:convert/convert.dart'; import 'package:json_path/json_path.dart'; import 'package:json_schema/json_schema.dart'; -/// PresentationDefinition represents a DIF Presentation Definition defined [here]. +/// PresentationDefinition represents a DIF Presentation Definition defined +/// [here](https://identity.foundation/presentation-exchange/#presentation-definition). /// Presentation Definitions are objects that articulate what proofs a Verifier requires. -/// -/// [here]: https://identity.foundation/presentation-exchange/#presentation-definition class PresentationDefinition { String id; String? name; @@ -37,7 +37,7 @@ class PresentationDefinition { 'name': name, 'purpose': purpose, 'input_descriptors': - List.from(inputDescriptors.map((x) => x.toJson())), + inputDescriptors.map((ind) => ind.toJson()).toList(), }; List selectCredentials(List vcJwts) { @@ -62,10 +62,9 @@ class _TokenizedField { _TokenizedField({required this.paths, required this.token}); } -/// InputDescriptor represents a DIF Input Descriptor defined [here]. +/// InputDescriptor represents a DIF Input Descriptor defined +/// [here](https://identity.foundation/presentation-exchange/#input-descriptor). /// Input Descriptors are used to describe the information a Verifier requires of a Holder. -/// -/// [here]: https://identity.foundation/presentation-exchange/#input-descriptor class InputDescriptor { String id; String? name; @@ -103,9 +102,9 @@ class InputDescriptor { return hex.encode(bytes); } - List selectCredentials(List vcJWTs) { + List selectCredentials(List vcJwts) { final List answer = []; - final List<_TokenizedField> tokenizedField = []; + final List<_TokenizedField> tokenizedFields = []; final schemaMap = { '\$schema': 'http://json-schema.org/draft-07/schema#', 'type': 'object', @@ -116,18 +115,13 @@ class InputDescriptor { // Populate JSON schema and generate tokens for each field for (var field in constraints.fields ?? []) { final token = _generateRandomToken(); - tokenizedField + tokenizedFields .add(_TokenizedField(token: token, paths: field.path ?? [])); final properties = schemaMap['properties'] as Map; if (field.filter != null) { properties[token] = field.filter.toJson(); - } else { - final anyType = { - 'type': ['string', 'number', 'boolean', 'object', 'array'], - }; - properties[token] = anyType; } final required = schemaMap['required'] as List; required.add(token); @@ -135,18 +129,16 @@ class InputDescriptor { final jsonSchema = JsonSchema.create(schemaMap); // Tokenize each vcJwt and validate it against the JSON schema - for (var vcJWT in vcJWTs) { - final decoded = json.decode(vcJWT); + for (var vcJwt in vcJwts) { + final decoded = json.decode(vcJwt); final selectionCandidate = {}; - for (var tokenPath in tokenizedField) { - for (var path in tokenPath.paths) { - final value = JsonPath(path) - .read(decoded) - .firstOrNull; // Custom function needed to handle JSON paths. + for (final tokenizedField in tokenizedFields) { + for (var path in tokenizedField.paths) { + final value = JsonPath(path).read(decoded).firstOrNull; if (value != null) { - selectionCandidate[tokenPath.token] = value; + selectionCandidate[tokenizedField.token] = value; break; } } @@ -154,7 +146,7 @@ class InputDescriptor { final validationResult = jsonSchema.validate(selectionCandidate); if (validationResult.isValid) { - answer.add(vcJWT); + answer.add(vcJwt); } } @@ -208,9 +200,8 @@ class Field { purpose: json['purpose'], filter: json['filter'] == null ? null : Filter.fromJson(json['filter']), optional: json['optional'], - predicate: json['predicate'] == null - ? null - : optionalityValues.map[json['predicate']], + predicate: Optionality.values + .firstWhereOrNull((val) => val.toString() == json['predicate']), ); Map toJson() => { @@ -220,18 +211,12 @@ class Field { 'purpose': purpose, 'filter': filter?.toJson(), 'optional': optional, - 'predicate': - predicate == null ? null : optionalityValues.reverse[predicate], + 'predicate': predicate?.toString(), }; } enum Optionality { required, preferred } -final optionalityValues = EnumValues({ - 'preferred': Optionality.preferred, - 'required': Optionality.required, -}); - /// Filter is a JSON Schema that is applied against the value of a field. class Filter { String? type; @@ -261,14 +246,3 @@ class Filter { 'contains': contains?.toJson(), }; } - -/// Helper class for handling enums in JSON. -// TODO might not need this -class EnumValues { - Map map; - Map reverseMap; - - EnumValues(this.map) : reverseMap = map.map((k, v) => MapEntry(v, k)); - - Map get reverse => reverseMap; -} diff --git a/packages/web5/lib/src/vc/vc.dart b/packages/web5/lib/src/vc/vc.dart index aa1b11d..29d368b 100644 --- a/packages/web5/lib/src/vc/vc.dart +++ b/packages/web5/lib/src/vc/vc.dart @@ -111,7 +111,8 @@ class VerifiableCredential { final credentialSubject = json['credentialSubject'] as Map; final subject = credentialSubject.remove('id'); final credentialSchema = (json['credentialSchema'] as List) - .map((e) => CredentialSchema(id: e['id'], type: e['type'])).toList(); + .map((e) => CredentialSchema(id: e['id'], type: e['type'])) + .toList(); final context = (json['@context'] as List).cast(); final type = (json['type'] as List).cast(); @@ -141,9 +142,11 @@ class VerifiableCredential { 'issuanceDate': issuanceDate, if (expirationDate != null) 'expirationDate': expirationDate, if (credentialSchema != null) - 'credentialSchema': credentialSchema!.map( - (e) => e.toJson(), - ).toList(), + 'credentialSchema': credentialSchema! + .map( + (e) => e.toJson(), + ) + .toList(), }; } } diff --git a/packages/web5/test/pexv2/pd_test.dart b/packages/web5/test/pexv2/pd_test.dart index 9966073..c6eb1b9 100644 --- a/packages/web5/test/pexv2/pd_test.dart +++ b/packages/web5/test/pexv2/pd_test.dart @@ -18,8 +18,8 @@ class SelectCredentialTestVector { }); factory SelectCredentialTestVector.fromJson(Map json) { - final input = Map.from(json['input']); - final output = Map.from(json['output']); + final input = Map.from(json['input']); + final output = Map.from(json['output']); return SelectCredentialTestVector( description: json['description'], @@ -40,7 +40,7 @@ void main() { getJsonVectors('presentation_exchange/select_credentials.json'); final vectorsJson2 = getJsonVectors('presentation_exchange/select_credentials_go.json'); - + final vectors = [...vectorsJson['vectors'], ...vectorsJson2['vectors']] .map((e) => SelectCredentialTestVector.fromJson(e)) .toList(); From 63d78118460a7eb350f11eb1c2c95bd637096356 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Fri, 3 May 2024 21:00:14 -0700 Subject: [PATCH 08/12] Rename file to presentation_definition.dart --- .../lib/src/pexv2/{pd.dart => presentation_definition.dart} | 0 .../pexv2/{pd_test.dart => presentation_definition_test.dart} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/web5/lib/src/pexv2/{pd.dart => presentation_definition.dart} (100%) rename packages/web5/test/pexv2/{pd_test.dart => presentation_definition_test.dart} (97%) diff --git a/packages/web5/lib/src/pexv2/pd.dart b/packages/web5/lib/src/pexv2/presentation_definition.dart similarity index 100% rename from packages/web5/lib/src/pexv2/pd.dart rename to packages/web5/lib/src/pexv2/presentation_definition.dart diff --git a/packages/web5/test/pexv2/pd_test.dart b/packages/web5/test/pexv2/presentation_definition_test.dart similarity index 97% rename from packages/web5/test/pexv2/pd_test.dart rename to packages/web5/test/pexv2/presentation_definition_test.dart index c6eb1b9..b43587f 100644 --- a/packages/web5/test/pexv2/pd_test.dart +++ b/packages/web5/test/pexv2/presentation_definition_test.dart @@ -1,5 +1,5 @@ import 'package:test/test.dart'; -import 'package:web5/src/pexv2/pd.dart'; +import 'package:web5/src/pexv2/presentation_definition.dart'; import '../helpers/test_vector_helpers.dart'; From 91fec93952f771270cbbe916f8a80ff38364bfe4 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Sat, 4 May 2024 12:11:10 -0700 Subject: [PATCH 09/12] Add json schema util class --- .../src/pexv2/presentation_definition.dart | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/web5/lib/src/pexv2/presentation_definition.dart b/packages/web5/lib/src/pexv2/presentation_definition.dart index 6c9964f..ad5193a 100644 --- a/packages/web5/lib/src/pexv2/presentation_definition.dart +++ b/packages/web5/lib/src/pexv2/presentation_definition.dart @@ -6,6 +6,29 @@ import 'package:convert/convert.dart'; import 'package:json_path/json_path.dart'; import 'package:json_schema/json_schema.dart'; +class _JsonSchema { + String schema = 'http://json-schema.org/draft-07/schema#'; + String type = 'object'; + Map properties = {}; + List required = []; + + _JsonSchema(); + + void addProperty(String name, Map property) { + properties[name] = property; + required.add(name); + } + + Map toJson() { + return { + '/$schema': schema, + 'type': type, + 'properties': properties, + 'required': required, + }; + } +} + /// PresentationDefinition represents a DIF Presentation Definition defined /// [here](https://identity.foundation/presentation-exchange/#presentation-definition). /// Presentation Definitions are objects that articulate what proofs a Verifier requires. @@ -105,12 +128,7 @@ class InputDescriptor { List selectCredentials(List vcJwts) { final List answer = []; final List<_TokenizedField> tokenizedFields = []; - final schemaMap = { - '\$schema': 'http://json-schema.org/draft-07/schema#', - 'type': 'object', - 'properties': {}, - 'required': [], - }; + final schema = _JsonSchema(); // Populate JSON schema and generate tokens for each field for (var field in constraints.fields ?? []) { @@ -118,15 +136,13 @@ class InputDescriptor { tokenizedFields .add(_TokenizedField(token: token, paths: field.path ?? [])); - final properties = schemaMap['properties'] as Map; - if (field.filter != null) { - properties[token] = field.filter.toJson(); + schema.addProperty(token, field.filter.toJson()); + } else { + schema.addProperty(token, {}); } - final required = schemaMap['required'] as List; - required.add(token); } - final jsonSchema = JsonSchema.create(schemaMap); + final jsonSchema = JsonSchema.create(schema.toJson()); // Tokenize each vcJwt and validate it against the JSON schema for (var vcJwt in vcJwts) { From fbb3dd0c8f76a88163a0a0a68e491b72fb4cd37e Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Mon, 6 May 2024 16:44:59 -0700 Subject: [PATCH 10/12] Update algorithm to remove a for loop --- .../src/pexv2/presentation_definition.dart | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/web5/lib/src/pexv2/presentation_definition.dart b/packages/web5/lib/src/pexv2/presentation_definition.dart index ad5193a..3d631a0 100644 --- a/packages/web5/lib/src/pexv2/presentation_definition.dart +++ b/packages/web5/lib/src/pexv2/presentation_definition.dart @@ -79,10 +79,10 @@ class PresentationDefinition { } class _TokenizedField { - List paths; + String path; String token; - _TokenizedField({required this.paths, required this.token}); + _TokenizedField({required this.path, required this.token}); } /// InputDescriptor represents a DIF Input Descriptor defined @@ -131,13 +131,14 @@ class InputDescriptor { final schema = _JsonSchema(); // Populate JSON schema and generate tokens for each field - for (var field in constraints.fields ?? []) { + for (Field field in constraints.fields ?? []) { final token = _generateRandomToken(); - tokenizedFields - .add(_TokenizedField(token: token, paths: field.path ?? [])); + for (String path in field.path ?? []) { + tokenizedFields.add(_TokenizedField(token: token, path: path)); + } if (field.filter != null) { - schema.addProperty(token, field.filter.toJson()); + schema.addProperty(token, field.filter!.toJson()); } else { schema.addProperty(token, {}); } @@ -151,13 +152,14 @@ class InputDescriptor { final selectionCandidate = {}; for (final tokenizedField in tokenizedFields) { - for (var path in tokenizedField.paths) { - final value = JsonPath(path).read(decoded).firstOrNull; - if (value != null) { - selectionCandidate[tokenizedField.token] = value; - break; - } - } + selectionCandidate[tokenizedField.token] ??= + JsonPath(tokenizedField.path).read(decoded).firstOrNull; + } + selectionCandidate.removeWhere((_, value) => value == null); + + if (selectionCandidate.keys.length < tokenizedFields.length) { + // Did not find values for all `field`s in the input desciptor + continue; } final validationResult = jsonSchema.validate(selectionCandidate); From df2fedb4e0537b75dfa3ecc4a5b9a86aaed0e33e Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Mon, 6 May 2024 16:46:16 -0700 Subject: [PATCH 11/12] Update var name --- .../lib/src/pexv2/presentation_definition.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/web5/lib/src/pexv2/presentation_definition.dart b/packages/web5/lib/src/pexv2/presentation_definition.dart index 3d631a0..b7c3359 100644 --- a/packages/web5/lib/src/pexv2/presentation_definition.dart +++ b/packages/web5/lib/src/pexv2/presentation_definition.dart @@ -78,11 +78,11 @@ class PresentationDefinition { } } -class _TokenizedField { +class _TokenizedPath { String path; String token; - _TokenizedField({required this.path, required this.token}); + _TokenizedPath({required this.path, required this.token}); } /// InputDescriptor represents a DIF Input Descriptor defined @@ -127,14 +127,14 @@ class InputDescriptor { List selectCredentials(List vcJwts) { final List answer = []; - final List<_TokenizedField> tokenizedFields = []; + final List<_TokenizedPath> tokenizedPaths = []; final schema = _JsonSchema(); // Populate JSON schema and generate tokens for each field for (Field field in constraints.fields ?? []) { final token = _generateRandomToken(); for (String path in field.path ?? []) { - tokenizedFields.add(_TokenizedField(token: token, path: path)); + tokenizedPaths.add(_TokenizedPath(token: token, path: path)); } if (field.filter != null) { @@ -151,13 +151,13 @@ class InputDescriptor { final selectionCandidate = {}; - for (final tokenizedField in tokenizedFields) { - selectionCandidate[tokenizedField.token] ??= - JsonPath(tokenizedField.path).read(decoded).firstOrNull; + for (final tokenizedPath in tokenizedPaths) { + selectionCandidate[tokenizedPath.token] ??= + JsonPath(tokenizedPath.path).read(decoded).firstOrNull; } selectionCandidate.removeWhere((_, value) => value == null); - if (selectionCandidate.keys.length < tokenizedFields.length) { + if (selectionCandidate.keys.length < tokenizedPaths.length) { // Did not find values for all `field`s in the input desciptor continue; } From 56d4f39598b23a9e6588cea31f8c6ed17652bfb2 Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 7 May 2024 15:38:20 -0700 Subject: [PATCH 12/12] Update submodule to latest main --- packages/web5/test/pexv2/presentation_definition_test.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web5/test/pexv2/presentation_definition_test.dart b/packages/web5/test/pexv2/presentation_definition_test.dart index b43587f..b826f19 100644 --- a/packages/web5/test/pexv2/presentation_definition_test.dart +++ b/packages/web5/test/pexv2/presentation_definition_test.dart @@ -38,10 +38,8 @@ void main() { group('vectors', () { final vectorsJson = getJsonVectors('presentation_exchange/select_credentials.json'); - final vectorsJson2 = - getJsonVectors('presentation_exchange/select_credentials_go.json'); - final vectors = [...vectorsJson['vectors'], ...vectorsJson2['vectors']] + final vectors = vectorsJson['vectors'] .map((e) => SelectCredentialTestVector.fromJson(e)) .toList();