Skip to content
This repository was archived by the owner on Dec 12, 2024. It is now read-only.

Commit 498defe

Browse files
authored
Fix signing (#26)
1 parent 87a8b6b commit 498defe

File tree

7 files changed

+199
-13
lines changed

7 files changed

+199
-13
lines changed

lib/src/protocol/crypto.dart

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
import 'dart:convert';
21
import 'dart:math';
32
import 'dart:typed_data';
43

54
import 'package:crypto/crypto.dart';
5+
import 'package:tbdex/src/protocol/jcs.dart';
66
import 'package:web5/web5.dart';
77

88
class CryptoUtils {
99
static Uint8List digest(Object value) {
10-
final payload = json.encode(value);
11-
final bytes = utf8.encode(payload);
12-
return Uint8List.fromList(sha256.convert(bytes).bytes);
10+
final canonicalized = JsonCanonicalizer.canonicalize(value);
11+
final digest = sha256.convert(canonicalized);
12+
13+
return Uint8List.fromList(digest.bytes);
1314
}
1415

1516
static String generateSalt() {
1617
var random = Random.secure();
17-
var bytes = Uint8List.fromList(List.generate(16, (_) => random.nextInt(256)));
18+
var bytes =
19+
Uint8List.fromList(List.generate(16, (_) => random.nextInt(256)));
1820
return Base64Url.encode(bytes);
1921
}
20-
}
22+
}

lib/src/protocol/jcs.dart

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'dart:convert';
2+
import 'dart:typed_data';
3+
4+
// TODO: turn into standalone lib
5+
6+
/// Implements the JSON Canonicalization Scheme specified in
7+
/// [RFC8785](https://www.rfc-editor.org/rfc/rfc8785)
8+
class JsonCanonicalizer {
9+
/// canonicalizes the provided input per
10+
/// [RFC8785](https://www.rfc-editor.org/rfc/rfc8785)
11+
static Uint8List canonicalize(Object? input, {StringBuffer? buffer}) {
12+
//! this weird line is here to catch any non json encodable types (e.g. fn)
13+
//! and error out if any are present. this could also be checked per term
14+
//! (key and value) during canonicalization. refactor to per term if/when
15+
//! we make this a standalone lib
16+
final o = jsonDecode(jsonEncode(input));
17+
18+
final sb = buffer ?? StringBuffer();
19+
_canonicalize(o, sb);
20+
21+
return utf8.encode(sb.toString());
22+
}
23+
24+
static void _canonicalize(Object? o, StringBuffer sb) {
25+
if (o == null || o is num || o is bool || o is String) {
26+
_writePrimitive(o, sb);
27+
} else if (o is List) {
28+
_writeList(o, sb);
29+
} else if (o is Map) {
30+
_writeMap(o, sb);
31+
}
32+
}
33+
34+
/// Writes a primitive value to the `StringBuffer`.
35+
static void _writePrimitive(Object? value, StringBuffer sb) {
36+
sb.write(json.encode(value));
37+
}
38+
39+
/// Writes a list to the `StringBuffer` using recursive serialization for elements.
40+
static void _writeList(List<dynamic> list, StringBuffer sb) {
41+
sb.write('[');
42+
var isFirst = true;
43+
44+
for (final item in list) {
45+
if (!isFirst) {
46+
sb.write(',');
47+
}
48+
49+
_canonicalize(item, sb);
50+
isFirst = false;
51+
}
52+
sb.write(']');
53+
}
54+
55+
/// Writes a map to the `StringBuffer` after sorting its keys.
56+
static void _writeMap(Map<dynamic, dynamic> map, StringBuffer sb) {
57+
sb.write('{');
58+
final keys = List<dynamic>.from(map.keys)..sort();
59+
60+
var isFirst = true;
61+
for (final key in keys) {
62+
if (!isFirst) {
63+
sb.write(',');
64+
}
65+
66+
var keyStr = key;
67+
if (key is! String) {
68+
keyStr = key.toString();
69+
}
70+
71+
sb.write('${json.encode(keyStr)}:');
72+
_canonicalize(map[key], sb);
73+
isFirst = false;
74+
}
75+
76+
sb.write('}');
77+
}
78+
}

lib/src/protocol/models/message.dart

+9-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ class MessageMetadata extends Metadata {
4242
return MessageMetadata(
4343
kind: MessageKind.values.firstWhere(
4444
(kind) => kind.name == json['kind'],
45-
orElse: () => throw TbdexParseException(TbdexExceptionCode.messageUnknownKind, 'unknown message kind: ${json['kind']}'),
45+
orElse: () => throw TbdexParseException(
46+
TbdexExceptionCode.messageUnknownKind,
47+
'unknown message kind: ${json['kind']}',
48+
),
4649
),
4750
to: json['to'],
4851
from: json['from'],
@@ -105,7 +108,7 @@ abstract class Message {
105108

106109
if (signingDid != metadata.from) {
107110
throw TbdexSignatureVerificationException(
108-
TbdexExceptionCode.messageSignatureMismatch,
111+
TbdexExceptionCode.messageSignatureMismatch,
109112
'signature verification failed: was not signed by the expected DID',
110113
);
111114
}
@@ -114,7 +117,10 @@ abstract class Message {
114117
}
115118

116119
Uint8List _digest() {
117-
return CryptoUtils.digest({'metadata': metadata, 'data': data});
120+
return CryptoUtils.digest({
121+
'metadata': metadata.toJson(),
122+
'data': data.toJson(),
123+
});
118124
}
119125

120126
@override

lib/src/protocol/models/message_data.dart

+5
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class RfqData extends MessageData {
107107
);
108108
}
109109

110+
@override
110111
Map<String, dynamic> toJson() {
111112
return {
112113
'offeringId': offeringId,
@@ -185,6 +186,7 @@ class QuoteData extends MessageData {
185186
);
186187
}
187188

189+
@override
188190
Map<String, dynamic> toJson() {
189191
return {
190192
'expiresAt': expiresAt,
@@ -269,6 +271,7 @@ class CloseData extends MessageData {
269271
);
270272
}
271273

274+
@override
272275
Map<String, dynamic> toJson() {
273276
return {
274277
if (success != null) 'success': success,
@@ -278,6 +281,7 @@ class CloseData extends MessageData {
278281
}
279282

280283
class OrderData extends MessageData {
284+
@override
281285
Map<String, dynamic> toJson() {
282286
return {};
283287
}
@@ -294,6 +298,7 @@ class OrderStatusData extends MessageData {
294298
);
295299
}
296300

301+
@override
297302
Map<String, dynamic> toJson() {
298303
return {
299304
'orderStatus': orderStatus,

lib/src/protocol/models/resource_data.dart

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import 'package:json_schema/json_schema.dart';
22

3-
abstract class Data {}
3+
abstract class Data {
4+
Map<String, dynamic> toJson() {
5+
throw UnimplementedError();
6+
}
7+
}
48

59
abstract class ResourceData extends Data {}
610

@@ -29,6 +33,7 @@ class OfferingData extends ResourceData {
2933
);
3034
}
3135

36+
@override
3237
Map<String, dynamic> toJson() {
3338
return {
3439
'description': description,

test/protocol/jcs_test.dart

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'dart:convert';
2+
import 'package:tbdex/src/protocol/jcs.dart';
3+
import 'package:test/test.dart';
4+
5+
void main() {
6+
group('JsonCanonicalizer', () {
7+
test('empty array', () {
8+
final result = JsonCanonicalizer.canonicalize([]);
9+
expect(utf8.decode(result), equals('[]'));
10+
});
11+
12+
test('one element array', () {
13+
final result = JsonCanonicalizer.canonicalize([123]);
14+
expect(utf8.decode(result), equals('[123]'));
15+
});
16+
17+
test('multi element array', () {
18+
final result = JsonCanonicalizer.canonicalize([123, 456, 'hello']);
19+
expect(utf8.decode(result), equals('[123,456,"hello"]'));
20+
});
21+
22+
test('null and undefined values in array', () {
23+
final result = JsonCanonicalizer.canonicalize([null, null, 'hello']);
24+
expect(utf8.decode(result), equals('[null,null,"hello"]'));
25+
});
26+
27+
test('object in array', () {
28+
final result = JsonCanonicalizer.canonicalize([
29+
{'b': 123, 'a': 'string'}
30+
]);
31+
expect(utf8.decode(result), equals('[{"a":"string","b":123}]'));
32+
});
33+
34+
test('empty object', () {
35+
final result = JsonCanonicalizer.canonicalize({});
36+
expect(utf8.decode(result), equals('{}'));
37+
});
38+
39+
test('object with one property', () {
40+
final result = JsonCanonicalizer.canonicalize({'hello': 'world'});
41+
expect(utf8.decode(result), equals('{"hello":"world"}'));
42+
});
43+
44+
test('object with more than one property', () {
45+
final result =
46+
JsonCanonicalizer.canonicalize({'hello': 'world', 'number': 123});
47+
expect(utf8.decode(result), equals('{"hello":"world","number":123}'));
48+
});
49+
50+
test('null', () {
51+
final result = JsonCanonicalizer.canonicalize(null);
52+
expect(utf8.decode(result), equals('null'));
53+
});
54+
55+
test('object with number key', () {
56+
expect(
57+
() => JsonCanonicalizer.canonicalize({42: 'foo'}),
58+
throwsA(isA<JsonUnsupportedObjectError>()),
59+
);
60+
});
61+
62+
test('object with a function', () {
63+
var customObject = {
64+
'a': 123,
65+
'b': 456,
66+
'toJSON': () => {'b': 456, 'a': 123},
67+
};
68+
69+
expect(
70+
() => JsonCanonicalizer.canonicalize(customObject),
71+
throwsA(isA<JsonUnsupportedObjectError>()),
72+
);
73+
});
74+
75+
// Handling error cases
76+
test('NaN in array', () {
77+
expect(
78+
() => JsonCanonicalizer.canonicalize([double.nan]),
79+
throwsA(isA<JsonUnsupportedObjectError>()),
80+
);
81+
});
82+
83+
test('Infinity in array', () {
84+
expect(
85+
() => JsonCanonicalizer.canonicalize([double.infinity]),
86+
throwsA(isA<JsonUnsupportedObjectError>()),
87+
);
88+
});
89+
});
90+
}

test/protocol/models/rfq_test.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ void main() async {
1414
await TestData.initializeDids();
1515

1616
group('Rfq', () {
17-
test('can create a new rfq', () {
17+
test('can create a new rfq', () async {
1818
final rfq = Rfq.create(
19-
TestData.pfi,
20-
TestData.alice,
19+
TestData.pfiDid.uri,
20+
TestData.aliceDid.uri,
2121
CreateRfqData(
2222
offeringId: TypeId.generate(ResourceKind.offering.name),
2323
payin: CreateSelectedPayinMethod(amount: '100', kind: 'BTC_ADDRESS'),

0 commit comments

Comments
 (0)