diff --git a/packages/web5/lib/src/dids/bearer_did.dart b/packages/web5/lib/src/dids/bearer_did.dart index 78ab336..c871880 100644 --- a/packages/web5/lib/src/dids/bearer_did.dart +++ b/packages/web5/lib/src/dids/bearer_did.dart @@ -20,11 +20,13 @@ class BearerDid { String uri; KeyManager keyManager; DidDocument document; + DidDocumentMetadata metadata; BearerDid({ required this.uri, required this.keyManager, required this.document, + this.metadata = const DidDocumentMetadata(), }); Future export() async { diff --git a/packages/web5/lib/src/dids/did_core/did_document_metadata.dart b/packages/web5/lib/src/dids/did_core/did_document_metadata.dart index f866252..6843099 100644 --- a/packages/web5/lib/src/dids/did_core/did_document_metadata.dart +++ b/packages/web5/lib/src/dids/did_core/did_document_metadata.dart @@ -51,7 +51,7 @@ class DidDocumentMetadata { /// the scope of the containing DID document. final String? canonicalId; - DidDocumentMetadata({ + const DidDocumentMetadata({ this.created, this.updated, this.deactivated, diff --git a/packages/web5/lib/src/dids/did_core/did_resolution_result.dart b/packages/web5/lib/src/dids/did_core/did_resolution_result.dart index 40eab5b..2290cd1 100644 --- a/packages/web5/lib/src/dids/did_core/did_resolution_result.dart +++ b/packages/web5/lib/src/dids/did_core/did_resolution_result.dart @@ -2,6 +2,27 @@ import 'package:web5/src/dids/did_core/did_document.dart'; import 'package:web5/src/dids/did_core/did_document_metadata.dart'; import 'package:web5/src/dids/did_core/did_resolution_metadata.dart'; +enum DidResolutionError { + invalidDid, + notFound, + representationNotSupported, +} + +extension ResolutionErrorValue on DidResolutionError { + String get value { + switch (this) { + case DidResolutionError.invalidDid: + return 'invalidDid'; + case DidResolutionError.notFound: + return 'notFound'; + case DidResolutionError.representationNotSupported: + return 'representationNotSupported'; + default: + return 'unknown'; + } + } +} + /// A class representing the result of a DID (Decentralized Identifier) /// resolution. /// @@ -61,13 +82,16 @@ class DidResolutionResult { didResolutionMetadata ?? DidResolutionMetadata(), didDocumentMetadata = didDocumentMetadata ?? DidDocumentMetadata(); - /// A convenience constructor for creating a [DidResolutionResult] representing + /// A factory constructor for creating a [DidResolutionResult] representing /// an invalid DID scenario. This sets the resolution metadata error to 'invalidDid' /// and leaves the DID document as `null`. - DidResolutionResult.invalidDid() - : didResolutionMetadata = DidResolutionMetadata(error: 'invalidDid'), - didDocument = null, - didDocumentMetadata = DidDocumentMetadata(); + factory DidResolutionResult.withError(DidResolutionError err) { + return DidResolutionResult( + didResolutionMetadata: DidResolutionMetadata(error: err.value), + didDocument: null, + didDocumentMetadata: DidDocumentMetadata(), + ); + } /// Converts this [DidResolutionResult] instance to a JSON map. /// diff --git a/packages/web5/lib/src/dids/did_core/did_verification_method.dart b/packages/web5/lib/src/dids/did_core/did_verification_method.dart index 5d5a2f5..f747d1f 100644 --- a/packages/web5/lib/src/dids/did_core/did_verification_method.dart +++ b/packages/web5/lib/src/dids/did_core/did_verification_method.dart @@ -1,5 +1,6 @@ import 'package:web5/src/crypto.dart'; import 'package:web5/src/dids/did_core/did_resource.dart'; +import 'package:web5/src/dids/did_core/did_verification_relationship.dart'; /// A DID document can express verification methods, such as cryptographic /// public keys, which can be used to authenticate or authorize interactions @@ -52,3 +53,19 @@ class DidVerificationMethod implements DidResource { ); } } + +class DidCreateVerificationMethod { + DidCreateVerificationMethod({ + required this.algorithm, + required this.controller, + this.id, + required this.purposes, + required this.type, + }); + + final AlgorithmId algorithm; + final String controller; + final String? id; + final List purposes; + final String type; +} diff --git a/packages/web5/lib/src/dids/did_dht/did_dht.dart b/packages/web5/lib/src/dids/did_dht/did_dht.dart index 292c67d..2607502 100644 --- a/packages/web5/lib/src/dids/did_dht/did_dht.dart +++ b/packages/web5/lib/src/dids/did_dht/did_dht.dart @@ -19,7 +19,7 @@ class DidDht { String relayUrl = 'https://diddht.tbddev.org', }) async { if (did.method != methodName) { - return DidResolutionResult.invalidDid(); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final parsedRelayUrl = Uri.parse(relayUrl); @@ -42,7 +42,7 @@ class DidDht { // final seq = bytes.sublist(64, 72); if (bytes.length < 72) { - return DidResolutionResult.invalidDid(); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final v = bytes.sublist(72); @@ -68,7 +68,7 @@ class DidDht { if (rootRecord == null) { // TODO: figure out more appopriate resolution error to use. - return DidResolutionResult.invalidDid(); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final Map> relationshipsMap = {}; @@ -77,7 +77,7 @@ class DidDht { if (splitEntry.length != 2) { // TODO: figure out more appopriate resolution error to use. - return DidResolutionResult.invalidDid(); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final [property, values] = splitEntry; diff --git a/packages/web5/lib/src/dids/did_jwk/did_jwk.dart b/packages/web5/lib/src/dids/did_jwk/did_jwk.dart index c4b0034..4d15a98 100644 --- a/packages/web5/lib/src/dids/did_jwk/did_jwk.dart +++ b/packages/web5/lib/src/dids/did_jwk/did_jwk.dart @@ -60,9 +60,9 @@ class DidJwk { /// an invalid [DidResolutionResult]. /// /// Throws [FormatException] if the JWK parsing fails. - static Future resolve(Did did) { + static Future resolve(Did did) async { if (did.method != methodName) { - return Future.value(DidResolutionResult.invalidDid()); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final dynamic jwk; @@ -70,7 +70,7 @@ class DidJwk { try { jwk = json.fromBase64Url(did.id); } on FormatException { - return Future.value(DidResolutionResult.invalidDid()); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final Jwk parsedJwk; @@ -78,13 +78,13 @@ class DidJwk { try { parsedJwk = Jwk.fromJson(jwk); } on Exception { - return Future.value(DidResolutionResult.invalidDid()); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } final didDocument = _createDidDocument(did, parsedJwk); final didResolutionResult = DidResolutionResult(didDocument: didDocument); - return Future.value(didResolutionResult); + return didResolutionResult; } static DidDocument _createDidDocument(Did did, Jwk jwk) { diff --git a/packages/web5/lib/src/dids/did_resolver.dart b/packages/web5/lib/src/dids/did_resolver.dart index 452b372..7c35850 100644 --- a/packages/web5/lib/src/dids/did_resolver.dart +++ b/packages/web5/lib/src/dids/did_resolver.dart @@ -52,7 +52,9 @@ class DidResolver { try { did = Did.parse(uri); } catch (e) { - return Future.value(DidResolutionResult.invalidDid()); + return Future.value( + DidResolutionResult.withError(DidResolutionError.invalidDid), + ); } final resolver = methodResolvers[did.method]; diff --git a/packages/web5/lib/src/dids/did_web/did_web.dart b/packages/web5/lib/src/dids/did_web/did_web.dart index db55f1c..b3408e5 100644 --- a/packages/web5/lib/src/dids/did_web/did_web.dart +++ b/packages/web5/lib/src/dids/did_web/did_web.dart @@ -1,49 +1,121 @@ import 'dart:convert'; import 'dart:io'; +import 'package:web5/src/crypto.dart'; +import 'package:web5/src/dids/bearer_did.dart'; +import 'package:web5/src/dids/did.dart'; import 'package:web5/src/dids/did_core.dart'; import 'package:web5/src/dids/did_method_resolver.dart'; -import 'package:web5/src/dids/did.dart'; class DidWeb { static const String methodName = 'web'; - static final resolver = DidMethodResolver(name: methodName, resolve: resolve); + + static final DidMethodResolver resolver = DidMethodResolver( + name: methodName, + resolve: resolve, + ); + + static Future create({ + required String url, + AlgorithmId? algorithm, + KeyManager? keyManager, + List? alsoKnownAs, + List? controllers, + List? services, + List? verificationMethods, + DidDocumentMetadata? metadata, + }) async { + algorithm ??= AlgorithmId.ed25519; + keyManager ??= InMemoryKeyManager(); + + final parsed = Uri.tryParse(url); + if (parsed == null) throw 'Unable to parse url $url'; + final String didId = + 'did:web:${parsed.host}${parsed.pathSegments.join(':')}'; + + final DidDocument doc = DidDocument( + id: didId, + alsoKnownAs: alsoKnownAs, + controller: controllers ?? didId, + ); + + final List defaultMethods = [ + DidCreateVerificationMethod( + algorithm: algorithm, + id: '0', + type: 'JsonWebKey', + controller: didId, + purposes: [ + VerificationPurpose.authentication, + VerificationPurpose.assertionMethod, + VerificationPurpose.capabilityDelegation, + VerificationPurpose.capabilityInvocation, + ], + ), + ]; + + final List methodsToAdd = + verificationMethods ?? defaultMethods; + + for (final DidCreateVerificationMethod vm in methodsToAdd) { + final String alias = await keyManager.generatePrivateKey(vm.algorithm); + final Jwk publicKey = await keyManager.getPublicKey(alias); + + final String methodId = '$didId#${vm.id}'; + doc.addVerificationMethod( + DidVerificationMethod( + id: methodId, + type: vm.type, + controller: vm.controller, + publicKeyJwk: publicKey, + ), + ); + + for (final VerificationPurpose purpose in vm.purposes) { + doc.addVerificationPurpose(purpose, methodId); + } + } + + for (final DidService service in (services ?? [])) { + doc.addService(service); + } + + return BearerDid( + uri: didId, + keyManager: keyManager, + document: doc, + metadata: metadata ?? DidDocumentMetadata(), + ); + } static Future resolve( Did did, { HttpClient? client, }) async { if (did.method != methodName) { - return DidResolutionResult.invalidDid(); + return DidResolutionResult.withError(DidResolutionError.invalidDid); } - // TODO: http technically not supported. remove after temp use - var resolutionUrl = did.id.replaceAll(':', '/'); - if (resolutionUrl.contains('localhost')) { - resolutionUrl = 'http://$resolutionUrl'; - } else { - resolutionUrl = 'https://$resolutionUrl'; - } + final String documentUrl = Uri.decodeFull(did.id.replaceAll(':', '/')); + Uri? didUri = Uri.tryParse('https://$documentUrl'); - if (Uri.parse(resolutionUrl).path.isEmpty) { - resolutionUrl = '$resolutionUrl/.well-known'; - } + if (didUri == null) throw 'Unable to parse DID document Url $documentUrl'; - resolutionUrl = Uri.decodeFull('$resolutionUrl/did.json'); - final parsedUrl = Uri.parse(resolutionUrl); + // If none was specified, use the default path. + if (didUri.path.isEmpty) didUri = didUri.replace(path: '/.well-known'); + didUri = didUri.replace(pathSegments: [...didUri.pathSegments, 'did.json']); - final httpClient = client ??= HttpClient(); - final request = await httpClient.getUrl(parsedUrl); - final response = await request.close(); + final HttpClient httpClient = client ??= HttpClient(); + final HttpClientRequest request = await httpClient.getUrl(didUri); + final HttpClientResponse response = await request.close(); if (response.statusCode != 200) { - // TODO: change this to something more appropriate - return DidResolutionResult.invalidDid(); + return DidResolutionResult.withError(DidResolutionError.notFound); } - final str = await response.transform(utf8.decoder).join(); - final jsonParsed = json.decode(str); - final doc = DidDocument.fromJson(jsonParsed); + final String str = await response.transform(utf8.decoder).join(); + final dynamic jsonParsed = json.decode(str); + final DidDocument doc = DidDocument.fromJson(jsonParsed); return DidResolutionResult(didDocument: doc); } diff --git a/packages/web5/lib/src/dids/portable_did.dart b/packages/web5/lib/src/dids/portable_did.dart index 3657d61..427d361 100644 --- a/packages/web5/lib/src/dids/portable_did.dart +++ b/packages/web5/lib/src/dids/portable_did.dart @@ -12,16 +12,16 @@ class PortableDid { List? privateKeys, }) : privateKeys = privateKeys ?? []; - factory PortableDid.fromJson(Map json) { + factory PortableDid.fromMap(Map map) { return PortableDid( - uri: json['uri'], - document: DidDocument.fromJson(json['document']), + uri: map['uri'], + document: DidDocument.fromJson(map['document']), privateKeys: - (json['privateKeys'] as List).map((e) => Jwk.fromJson(e)).toList(), + (map['privateKeys'] as List).map((e) => Jwk.fromJson(e)).toList(), ); } - Map toJson() { + Map get map { return { 'uri': uri, 'document': document.toJson(), diff --git a/packages/web5/lib/src/jwt/jwt_decoded.dart b/packages/web5/lib/src/jwt/jwt_decoded.dart deleted file mode 100644 index 77ff57f..0000000 --- a/packages/web5/lib/src/jwt/jwt_decoded.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:web5/src/jwt/jwt_claims.dart'; -import 'package:web5/src/jwt/jwt_header.dart'; - -/// Represents a decoded JWT, including both its header and payload. -/// -/// **Note**: Signature not included because its decoded form would be bytes -class JwtDecoded { - final JwtHeader header; - final JwtClaims payload; - - JwtDecoded({required this.header, required this.payload}); - - factory JwtDecoded.fromJson(Map json) { - return JwtDecoded( - header: JwtHeader.fromJson(json['header']), - payload: JwtClaims.fromJson(json['payload']), - ); - } - - Map toJson() { - return { - 'header': header.toJson(), - 'payload': payload.toJson(), - }; - } -} diff --git a/packages/web5/test/dids/did_web_test.dart b/packages/web5/test/dids/did_web_test.dart index f10487f..16c7435 100644 --- a/packages/web5/test/dids/did_web_test.dart +++ b/packages/web5/test/dids/did_web_test.dart @@ -3,9 +3,7 @@ import 'dart:io'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; -import 'package:web5/src/dids/did.dart'; -import 'package:web5/src/dids/did_core/did_resolution_result.dart'; -import 'package:web5/src/dids/did_web/did_web.dart'; +import 'package:web5/web5.dart'; class MockHttpClient extends Mock implements HttpClient {} @@ -78,20 +76,37 @@ void main() { }); group('DidWeb', () { + test('should be created successfully', () async { + final BearerDid did = await DidWeb.create( + algorithm: AlgorithmId.ed25519, + keyManager: InMemoryKeyManager(), + url: 'www.linkedin.com/user123', + ); + + expect('did:web:www.linkedin.com:user123', did.document.id); + }); + test('should return invalid did with wrong method', () async { final did = Did.parse('did:bad:www.linkedin.com'); final result = await DidWeb.resolve(did); - expect(result, DidResolutionResult.invalidDid()); + expect( + result, + DidResolutionResult.withError(DidResolutionError.invalidDid), + ); }); - test('should return invalid did with failed http request', () async { + test('should return did with failed http request', () async { when(() => response.statusCode).thenReturn(400); when(() => request.close()).thenAnswer((_) async => response); when(() => mockClient.getUrl(any())).thenAnswer((_) async => request); final did = Did.parse('did:web:www.linkedin.com'); final result = await DidWeb.resolve(did, client: mockClient); - expect(result, DidResolutionResult.invalidDid()); + + expect( + result, + DidResolutionResult.withError(DidResolutionError.notFound), + ); }); test('should resolve successfully', () async { @@ -124,11 +139,11 @@ void main() { when(() => request.close()).thenAnswer((_) async => response); when( () => mockClient.getUrl( - Uri.parse('http://localhost:8892/ingress/did.json'), + Uri.parse('https://www.remotehost.com:8892/ingress/did.json'), ), ).thenAnswer((_) async => request); - final did = Did.parse('did:web:localhost%3A8892:ingress'); + final did = Did.parse('did:web:www.remotehost.com%3A8892:ingress'); final result = await DidWeb.resolve( did, client: mockClient, @@ -136,8 +151,9 @@ void main() { expect(result.didDocument, isNotNull); verify( - () => mockClient - .getUrl(Uri.parse('http://localhost:8892/ingress/did.json')), + () => mockClient.getUrl( + Uri.parse('https://www.remotehost.com:8892/ingress/did.json'), + ), ); }); });