Skip to content

Commit

Permalink
vc package (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
diehuxx authored Apr 30, 2024
1 parent 12cb89c commit ab92c4b
Show file tree
Hide file tree
Showing 5 changed files with 629 additions and 0 deletions.
149 changes: 149 additions & 0 deletions packages/web5/lib/src/vc/vc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import 'package:uuid/uuid.dart';
import 'package:web5/web5.dart';

class CredentialSchema {
String id;
String? type;

CredentialSchema({
required this.id,
this.type,
});

Map<String, dynamic> toJson() {
return {
'type': type,
'id': id,
};
}
}

class VerifiableCredential {
// https://www.w3.org/TR/vc-data-model/#contexts
static final baseContext = 'https://www.w3.org/2018/credentials/v1';
// https://www.w3.org/TR/vc-data-model/#dfn-type
static final baseType = 'VerifiableCredential';

// https://www.w3.org/TR/vc-data-model/#contexts
List<String> context;
// https://www.w3.org/TR/vc-data-model/#dfn-type
List<String> type;
// https://www.w3.org/TR/vc-data-model/#issuer
String issuer;
// https://www.w3.org/TR/vc-data-model/#credential-subject
String subject;
Map<String, dynamic> data;
// https://www.w3.org/TR/vc-data-model/#identifiers
String id;
// https://www.w3.org/TR/vc-data-model/#issuance-date
String issuanceDate;
// https://www.w3.org/TR/vc-data-model/#expiration
String? expirationDate;
// https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
List<CredentialSchema>? credentialSchema;

VerifiableCredential._({
required this.context,
required this.type,
required this.issuer,
required this.subject,
required this.data,
required this.issuanceDate,
required this.id,
this.expirationDate,
this.credentialSchema = const [],
});

static VerifiableCredential create({
required String issuer,
required String subject,
required Map<String, dynamic> data,
List<String>? context,
List<String>? type,
String? id,
DateTime? issuanceDate,
DateTime? expirationDate,
List<CredentialSchema> credentialSchema = const [],
}) {
final uuid = Uuid();

context = context ?? [baseContext];
type = type ?? [baseType];
id = id ?? 'urn:vc:uuid:${uuid.v4()}';
issuanceDate = issuanceDate ?? DateTime.now();

return VerifiableCredential._(
context: context,
type: type,
issuer: issuer,
subject: subject,
data: data,
id: id,
issuanceDate: issuanceDate.toString(),
expirationDate: expirationDate?.toString(),
credentialSchema: credentialSchema,
);
}

Future<String> sign(
BearerDid bearerDid,
) async {
final claims = JwtClaims(
iss: issuer,
jti: id,
sub: subject,
);

final issuanceDateTime = DateTime.parse(issuanceDate);
claims.nbf = issuanceDateTime.millisecondsSinceEpoch ~/ 1000;

if (expirationDate != null) {
final expirationDateTime = DateTime.parse(expirationDate!);
claims.exp = expirationDateTime.millisecondsSinceEpoch ~/ 1000;
}

claims.misc = {'vc': toJson()};

return await Jwt.sign(did: bearerDid, payload: claims);
}

factory VerifiableCredential.fromJson(Map<String, dynamic> json) {
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();
final context = (json['@context'] as List<dynamic>).cast<String>();
final type = (json['type'] as List<dynamic>).cast<String>();

return VerifiableCredential._(
issuer: json['issuer'],
subject: subject,
data: credentialSubject,
id: json['id'],
context: context,
type: type,
issuanceDate: json['issuanceDate'],
expirationDate: json['expirationDate'],
credentialSchema: credentialSchema,
);
}

Map<String, dynamic> toJson() {
return {
'@context': context,
'type': type,
'issuer': issuer,
'credentialSubject': {
'id': subject,
...data,
},
'id': id,
'issuanceDate': issuanceDate,
if (expirationDate != null) 'expirationDate': expirationDate,
if (credentialSchema != null)
'credentialSchema': credentialSchema!.map(
(e) => e.toJson(),
).toList(),
};
}
}
95 changes: 95 additions & 0 deletions packages/web5/lib/src/vc/vc_jwt.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:web5/src/jwt.dart';
import 'package:web5/src/vc/vc.dart';

class DecodedVcJwt {
VerifiableCredential vc;
DecodedJwt jwt;

DecodedVcJwt(this.vc, this.jwt);

static DecodedVcJwt decode(String jwt) {
final decoded = Jwt.decode(jwt);

if (decoded.claims.misc == null || decoded.claims.misc!['vc'] == null) {
throw Exception('vc-jwt missing vc claims');
}

final vc = VerifiableCredential.fromJson(decoded.claims.misc!['vc']);

// the following conditionals are included to conform with the jwt decoding section
// of the specification defined here: https://www.w3.org/TR/vc-data-model/#jwt-decoding
if (decoded.claims.iss != null) {
vc.issuer = decoded.claims.iss!;
}

if (decoded.claims.jti != null) {
vc.id = decoded.claims.jti!;
}

if (decoded.claims.sub != null) {
vc.subject = decoded.claims.sub!;
}

if (decoded.claims.exp != null) {
vc.expirationDate =
DateTime.fromMillisecondsSinceEpoch(decoded.claims.exp! * 1000)
.toString();
}

if (decoded.claims.nbf != null) {
vc.issuanceDate =
DateTime.fromMillisecondsSinceEpoch(decoded.claims.nbf! * 1000)
.toString();
}

return DecodedVcJwt(vc, decoded);
}

Future<void> verify() async {
if (jwt.header.typ != 'JWT') {
throw Exception('Invalid typ, must be "JWT"');
}

if (vc.issuer == '') {
throw Exception('Missing issuer');
}

if (vc.id == '') {
throw Exception('Missing id');
}

final issuanceDateTime = DateTime.parse(vc.issuanceDate);
if (DateTime.now().isBefore(issuanceDateTime)) {
throw Exception('VC cannot be used before ${vc.issuanceDate}');
}

if (vc.expirationDate != null) {
final expirationDateTime = DateTime.parse(vc.expirationDate!);
if (DateTime.now().isAfter(expirationDateTime)) {
throw Exception('VC expired on ${vc.expirationDate}');
}
}

if (vc.type.isEmpty) {
throw Exception('Missing type');
}

if (!vc.type.contains(VerifiableCredential.baseType)) {
throw Exception(
'Missing base type: ${VerifiableCredential.baseContext}',
);
}

if (vc.context.isEmpty) {
throw Exception('Missing context');
}

if (!vc.context.contains(VerifiableCredential.baseContext)) {
throw Exception(
'Missing base context: ${VerifiableCredential.baseContext}',
);
}

await jwt.verify();
}
}
1 change: 1 addition & 0 deletions packages/web5/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies:
cryptography: ^2.7.0
pointycastle: ^3.7.3
http: ^1.2.0
uuid: ^4.4.0

dev_dependencies:
lints: ^3.0.0
Expand Down
Loading

0 comments on commit ab92c4b

Please sign in to comment.