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

Select Credentials #81

Merged
merged 12 commits into from
May 7, 2024
274 changes: 274 additions & 0 deletions packages/web5/lib/src/pexv2/pd.dart
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
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.
///
/// [here]: https://identity.foundation/presentation-exchange/#presentation-definition
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
class PresentationDefinition {
String id;
String? name;
String? purpose;
List<InputDescriptor> inputDescriptors;

PresentationDefinition({
required this.id,
this.name,
this.purpose,
required this.inputDescriptors,
});

factory PresentationDefinition.fromJson(Map<String, dynamic> json) =>
PresentationDefinition(
id: json['id'],
name: json['name'],
purpose: json['purpose'],
inputDescriptors: List<InputDescriptor>.from(
json['input_descriptors'].map((x) => InputDescriptor.fromJson(x)),
),
);

Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'purpose': purpose,
'input_descriptors':
List<dynamic>.from(inputDescriptors.map((x) => x.toJson())),
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
};

List<String> selectCredentials(List<String> vcJwts) {
final Set<String> matches = {};

for (final inputDescriptor in inputDescriptors) {
final matchingVcJwts = inputDescriptor.selectCredentials(vcJwts);
if (matchingVcJwts.isEmpty) {
return [];
}
matches.addAll(matchingVcJwts);
}

return matches.toList();
}
}

class _TokenizedField {
List<String> paths;
String token;

_TokenizedField({required this.paths, required this.token});
}

/// 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
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
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<String, dynamic> json) =>
InputDescriptor(
id: json['id'],
name: json['name'],
purpose: json['purpose'],
constraints: Constraints.fromJson(json['constraints']),
);

Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'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<String> selectCredentials(List<String> vcJWTs) {
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
final List<String> answer = [];
final List<_TokenizedField> tokenizedField = [];
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
final schemaMap = {
'\$schema': 'http://json-schema.org/draft-07/schema#',
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
'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<String, dynamic>;

if (field.filter != null) {
properties[token] = field.filter.toJson();
} else {
final anyType = {
'type': ['string', 'number', 'boolean', 'object', 'array'],
};
properties[token] = anyType;
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
}
final required = schemaMap['required'] as List<dynamic>;
required.add(token);
}
final jsonSchema = JsonSchema.create(schemaMap);

// Tokenize each vcJwt and validate it against the JSON schema
for (var vcJWT in vcJWTs) {
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
final decoded = json.decode(vcJWT);

final selectionCandidate = <String, dynamic>{};

for (var tokenPath in tokenizedField) {
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this loop ends and no paths matched, there's no need to continue evaluating the current vcJwt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

}

final validationResult = jsonSchema.validate(selectionCandidate);
if (validationResult.isValid) {
answer.add(vcJWT);
}
}

return answer;
}
}

/// Constraints contains the requirements for a given Input Descriptor.
class Constraints {
List<Field>? fields;

Constraints({this.fields});

factory Constraints.fromJson(Map<String, dynamic> json) => Constraints(
fields: json['fields'] == null
? null
: List<Field>.from(json['fields'].map((x) => Field.fromJson(x))),
);

Map<String, dynamic> toJson() => {
'fields': fields == null
? null
: List<dynamic>.from(fields!.map((x) => x.toJson())),
};
mistermoe marked this conversation as resolved.
Show resolved Hide resolved
}

/// Field contains the requirements for a given field within a proof.
class Field {
String? id;
String? name;
List<String>? 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<String, dynamic> json) => Field(
id: json['id'],
name: json['name'],
path: json['path'] == null ? null : List<String>.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<String, dynamic> 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<String, dynamic> json) => Filter(
type: json['type'],
pattern: json['pattern'],
constValue: json['const'],
contains:
json['contains'] == null ? null : Filter.fromJson(json['contains']),
);

Map<String, dynamic> toJson() => {
'type': type,
'pattern': pattern,
'const': constValue,
'contains': contains?.toJson(),
};
}

/// Helper class for handling enums in JSON.
// TODO might not need this
class EnumValues<T> {
Map<String, T> map;
Map<T, String> reverseMap;

EnumValues(this.map) : reverseMap = map.map((k, v) => MapEntry(v, k));

Map<T, String> get reverse => reverseMap;
}
2 changes: 2 additions & 0 deletions packages/web5/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ dependencies:
pointycastle: ^3.7.3
http: ^1.2.0
uuid: ^4.4.0
json_path: ^0.7.1
json_schema: ^5.1.7

dev_dependencies:
lints: ^3.0.0
Expand Down
12 changes: 8 additions & 4 deletions packages/web5/test/crypto/secp256k1_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,11 @@ void main() {
final file = File(vectorPath);
late List<TestVector> 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');
}

Expand All @@ -88,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');
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/web5/test/helpers/test_vector_helpers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'dart:convert';
import 'dart:io';

final thisDir = Directory.current.path;
final vectorDir = '$thisDir/../../web5-spec/test-vectors/';

Map<String, dynamic> getJsonVectors(String vectorSubPath) {
final vectorPath = '$vectorDir$vectorSubPath';
final file = File(vectorPath);

try {
final contents = file.readAsStringSync();
return json.decode(contents);
} catch (e) {
throw Exception('Failed to load verify test vectors: $e');
}
}
Loading
Loading