-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
629 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.