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

Refactor Jws and Jwt Verification #46

Merged
merged 3 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/web5/lib/src/jws/decoded_jws.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:typed_data';

import 'package:web5/src/crypto.dart';
import 'package:web5/src/jws/jws_header.dart';

import 'package:web5/src/dids.dart';

class DecodedJws {
final JwsHeader header;
final Uint8List payload;
final Uint8List signature;
final List<String> parts;

static final _didResolver =
DidResolver(methodResolvers: [DidJwk.resolver, DidDht.resolver]);

DecodedJws({
required this.header,
required this.payload,
required this.signature,
required this.parts,
});

Future<void> verify() async {
if (header.kid == null || header.alg == null) {
throw Exception(
'Malformed JWS. expected header to contain kid and alg.',
);
}

final dereferenceResult = await _didResolver.dereference(header.kid!);
if (dereferenceResult.hasError()) {
throw Exception(
'Verification failed. Failed to dereference kid. Error: ${dereferenceResult.dereferencingMetadata.error}',
);
}

final didResource = dereferenceResult.contentStream;
if (didResource == null) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

if (didResource is! DidVerificationMethod) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
Comment on lines +46 to +48
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a more specific exception here? Or are both of these changes for header kid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i think we can get slightly more specific. the first exception is thrown if no did resource exists with the kid provided. the second is thrown if the kid provided doesnt point to a VerificationMethod

}

final publicKeyJwk = didResource.publicKeyJwk;
final dsaName =
DsaName.findByAlias(algorithm: header.alg, curve: publicKeyJwk!.crv);
Comment on lines +52 to +53
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if it's a big worry here, but since we are reading the crv with a ! there's a chance that would throw when trying to read it and you wouldn't get the nice expectation below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

great point @wesbillman . i actually left this as is because i'm removing the entire concept of aliases in the next PR


if (dsaName == null) {
throw Exception('${header.alg}:${publicKeyJwk.crv} not supported.');
}

await DsaAlgorithms.verify(
algName: dsaName,
publicKey: publicKeyJwk,
payload: payload,
signature: signature,
);
}
}
134 changes: 53 additions & 81 deletions packages/web5/lib/src/jws/jws.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:web5/src/dids.dart';
import 'package:web5/src/crypto.dart';
import 'package:web5/src/extensions.dart';
import 'package:web5/src/dids.dart';
import 'package:web5/src/extensions/base64url.dart';
import 'package:web5/src/jws/decoded_jws.dart';
import 'package:web5/src/jws/jws_header.dart';

final _base64UrlCodec = Base64Codec.urlSafe();
Expand All @@ -14,6 +15,50 @@ class Jws {
static final _didResolver =
DidResolver(methodResolvers: [DidJwk.resolver, DidDht.resolver]);

static DecodedJws decode(String jws) {
final parts = jws.split('.');

if (parts.length != 3) {
throw Exception(
'Malformed JWT. expected 3 parts. got ${parts.length}',
);
}

final JwsHeader header;
try {
header = JwsHeader.fromBase64Url(parts[0]);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT header',
);
}

final Uint8List payload;
try {
payload = _base64UrlDecoder.convertNoPadding(parts[1]);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT payload',
);
}

final Uint8List signature;
try {
signature = base64.decoder.convertNoPadding(parts[2]);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT payload',
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be ...for JWT signature

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch! fixed ad71b40

);
}

return DecodedJws(
header: header,
payload: payload,
signature: signature,
parts: parts,
);
}

/// Signs a JWT payload using a specified [Did] and returns the signed JWT.
///
/// Throws [Exception] if any error occurs during the signing process.
Expand Down Expand Up @@ -71,86 +116,13 @@ class Jws {
}
}

static Future<void> verify(String compactJws, {Uint8List? payload}) async {
final splitJws = compactJws.split('.');

if (splitJws.length != 3) {
throw Exception(
'Malformed JWS. expected 3 parts. got ${splitJws.length}',
);
}

final [
base64UrlEncodedHeader,
base64UrlEncodedPayload,
base64UrlEncodedSignature
] = splitJws;

final JwsHeader header;
static Future<DecodedJws> verify(String jws) async {
try {
header = JwsHeader.fromBase64Url(base64UrlEncodedHeader);
} on Exception {
throw Exception(
'Malformed JWS. Invalid base64url encoding for JWS header',
);
}

if (header.kid == null || header.alg == null) {
throw Exception(
'Malformed JWS. expected header to contain kid and alg.',
);
}

try {
payload ??= _base64UrlDecoder.convertNoPadding(base64UrlEncodedPayload);
} on Exception {
throw Exception(
'Malformed JWS. Invalid base64url encoding for JWS payload',
);
}

final Uint8List signature;
try {
signature = _base64UrlDecoder.convertNoPadding(base64UrlEncodedSignature);
} on Exception {
throw Exception(
'Malformed JWS. Invalid base64url encoding for JWS signature',
);
final decodedJws = decode(jws);
await decodedJws.verify();
return decodedJws;
} on Exception catch (e) {
throw Exception('Verification failed. $e');
}

final dereferenceResult = await _didResolver.dereference(header.kid!);
if (dereferenceResult.hasError()) {
throw Exception(
'Verification failed. Failed to dereference kid. Error: ${dereferenceResult.dereferencingMetadata.error}',
);
}

final didResource = dereferenceResult.contentStream;
if (didResource == null) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

if (didResource is! DidVerificationMethod) {
throw Exception(
'Verification failed. Expected header kid to dereference a verification method',
);
}

final publicKeyJwk = didResource.publicKeyJwk;
final dsaName =
DsaName.findByAlias(algorithm: header.alg, curve: publicKeyJwk!.crv);

if (dsaName == null) {
throw Exception('${header.alg}:${publicKeyJwk.crv} not supported.');
}

return DsaAlgorithms.verify(
algName: dsaName,
publicKey: publicKeyJwk,
payload: payload,
signature: signature,
);
}
}
30 changes: 30 additions & 0 deletions packages/web5/lib/src/jwt/decoded_jwt.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:web5/src/jws/decoded_jws.dart';
import 'package:web5/web5.dart';

class DecodedJwt {
final JwtHeader header;
final JwtClaims claims;
final Uint8List signature;
final List<String> parts;

DecodedJwt({
required this.header,
required this.claims,
required this.signature,
required this.parts,
});

Future<void> verify() async {
final decodedJws = DecodedJws(
header: header,
payload: Base64Codec.urlSafe().decoder.convertNoPadding(parts[1]),
signature: signature,
parts: parts,
);

await decodedJws.verify();
}
}
90 changes: 18 additions & 72 deletions packages/web5/lib/src/jwt/jwt.dart
Original file line number Diff line number Diff line change
@@ -1,86 +1,32 @@
import 'dart:convert';

import 'package:web5/src/dids.dart';
import 'package:web5/src/extensions.dart';
import 'package:web5/src/jws.dart';
import 'package:web5/src/jws/jws.dart';
import 'package:web5/src/dids/did.dart';
import 'package:web5/src/jwt/decoded_jwt.dart';
import 'package:web5/src/jwt/jwt_claims.dart';
import 'package:web5/src/extensions/json.dart';
import 'package:web5/src/jwt/jwt_decoded.dart';
import 'package:web5/src/jwt/jwt_encoded.dart';
import 'package:web5/src/jwt/jwt_header.dart';

/**
* TODO: refactor. awkward implementation:
* * Jwt.parse() returns an instance of Jwt but you can't call sign on an
* an instance of Jwt. Likely makes most sense for Jwt to have static methods
* only and potentially return something like ParsedJwt instead
* * Jwt.verify calls Jwt.parse first then calls Jws.verify which effectively
* performs the same logic as Jwt.parse
*/

/// A utility class for handling
/// [JSON Web Tokens (JWTs)](https://datatracker.ietf.org/doc/html/rfc7519)
///
/// This class provides functionalities to parse, encode, and sign JWTs.
/// It supports JWT signing with DID keys.
class Jwt {
JwtEncoded encoded;
JwtDecoded decoded;

Jwt({required this.encoded, required this.decoded});

/// Parses a signed JWT string into its decoded form. returns both split
/// encoded and decoded forms
///
/// Throws [Exception] if the JWT is malformed or if it does not meet
/// the expected structure and encoding requirements.
factory Jwt.parse(String signedJwt) {
final splitJwt = signedJwt.split('.');

if (splitJwt.length != 3) {
throw Exception(
'Malformed JWT. expected 3 parts. got ${splitJwt.length}',
);
}

final [
base64UrlEncodedHeader,
base64UrlEncodedPayload,
base64UrlEncodedSignature
] = splitJwt;

final JwtHeader header;
try {
header = JwtHeader.fromBase64Url(base64UrlEncodedHeader);
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT header',
);
}
static DecodedJwt decode(String jwt) {
final decodedJws = Jws.decode(jwt);

if (header.typ == null || header.typ?.toUpperCase() != 'JWT') {
throw Exception('Expected JWT header to contain typ property set to JWT');
}

if (header.alg == null || header.kid == null) {
throw Exception('Expected JWT header to contain alg and kid');
}

final JwtClaims payload;
final JwtClaims claims;
try {
payload = JwtClaims.fromBase64Url(base64UrlEncodedPayload);
final str = utf8.decode(decodedJws.payload);
claims = JwtClaims.fromJson(json.decode(str));
} on Exception {
throw Exception(
'Malformed JWT. Invalid base64url encoding for JWT payload',
);
}

return Jwt(
decoded: JwtDecoded(header: header, payload: payload),
encoded: JwtEncoded(
header: base64UrlEncodedHeader,
payload: base64UrlEncodedPayload,
signature: base64UrlEncodedSignature,
),
return DecodedJwt(
header: decodedJws.header,
claims: claims,
signature: decodedJws.signature,
parts: decodedJws.parts,
);
}

Expand All @@ -97,9 +43,9 @@ class Jwt {
return Jws.sign(did: did, payload: payloadBytes, header: header);
}

static Future<void> verify(String signedJwt) async {
Jwt.parse(signedJwt);

return Jws.verify(signedJwt);
static Future<DecodedJwt> verify(String jwt) async {
final decodedJwt = decode(jwt);
await decodedJwt.verify();
return decodedJwt;
}
}
Loading
Loading