Skip to content

Commit

Permalink
Select Credentials (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
Diane Huxley authored May 7, 2024
1 parent 2807346 commit 44d0e96
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 9 deletions.
266 changes: 266 additions & 0 deletions packages/web5/lib/src/pexv2/presentation_definition.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
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';

class _JsonSchema {
String schema = 'http://json-schema.org/draft-07/schema#';
String type = 'object';
Map<String, dynamic> properties = {};
List<String> required = [];

_JsonSchema();

void addProperty(String name, Map<String, dynamic> property) {
properties[name] = property;
required.add(name);
}

Map<String, dynamic> 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.
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':
inputDescriptors.map((ind) => ind.toJson()).toList(),
};

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 _TokenizedPath {
String path;
String token;

_TokenizedPath({required this.path, required this.token});
}

/// 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.
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) {
final List<String> answer = [];
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 ?? []) {
tokenizedPaths.add(_TokenizedPath(token: token, path: path));
}

if (field.filter != null) {
schema.addProperty(token, field.filter!.toJson());
} else {
schema.addProperty(token, {});
}
}
final jsonSchema = JsonSchema.create(schema.toJson());

// Tokenize each vcJwt and validate it against the JSON schema
for (var vcJwt in vcJwts) {
final decoded = json.decode(vcJwt);

final selectionCandidate = <String, dynamic>{};

for (final tokenizedPath in tokenizedPaths) {
selectionCandidate[tokenizedPath.token] ??=
JsonPath(tokenizedPath.path).read(decoded).firstOrNull;
}
selectionCandidate.removeWhere((_, value) => value == null);

if (selectionCandidate.keys.length < tokenizedPaths.length) {
// Did not find values for all `field`s in the input desciptor
continue;
}

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())),
};
}

/// 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: Optionality.values
.firstWhereOrNull((val) => val.toString() == json['predicate']),
);

Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'path': path,
'purpose': purpose,
'filter': filter?.toJson(),
'optional': optional,
'predicate': predicate?.toString(),
};
}

enum Optionality { required, preferred }

/// 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(),
};
}
11 changes: 7 additions & 4 deletions packages/web5/lib/src/vc/vc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ class VerifiableCredential {
final credentialSubject = json['credentialSubject'] as Map<String, dynamic>;
final subject = credentialSubject.remove('id');
final credentialSchema = (json['credentialSchema'] as List<dynamic>)
.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<dynamic>).cast<String>();
final type = (json['type'] as List<dynamic>).cast<String>();

Expand Down Expand Up @@ -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(),
};
}
}
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

0 comments on commit 44d0e96

Please sign in to comment.