Skip to content

Commit 0d20068

Browse files
authored
Merge commit from fork
🔒️ Implement certificate's time and canister ranges checks
2 parents 47b2844 + c6668fc commit 0d20068

File tree

6 files changed

+145
-56
lines changed

6 files changed

+145
-56
lines changed

packages/agent_dart_base/lib/agent/agent/http/index.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ Future<T> withRetry<T>(
3939
}
4040
}
4141

42+
/// Most of the timeouts will happen in 5 minutes.
43+
const defaultExpireInMinutes = 5;
44+
const defaultExpireInDuration = Duration(minutes: defaultExpireInMinutes);
45+
4246
/// Default delta for ingress expiry is 5 minutes.
43-
const _defaultIngressExpiryDeltaInMilliseconds = 5 * 60 * 1000;
47+
const _defaultIngressExpiryDeltaInMilliseconds =
48+
defaultExpireInMinutes * 60 * 1000;
4449

4550
/// Root public key for the IC, encoded as hex
4651
const _icRootKey = '308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7'

packages/agent_dart_base/lib/agent/certificate.dart

Lines changed: 106 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ import 'package:typed_data/typed_data.dart';
55

66
import '../../utils/extension.dart';
77
import '../../utils/u8a.dart';
8+
import '../principal/principal.dart';
89
import 'agent/api.dart';
910
import 'bls.dart';
1011
import 'cbor.dart';
1112
import 'errors.dart';
1213
import 'request_id.dart';
1314
import 'types.dart';
15+
import 'utils/buffer_pipe.dart';
16+
import 'utils/leb128.dart';
1417

1518
final AgentBLS _bls = AgentBLS();
1619

1720
/// A certificate needs to be verified (using Certificate.prototype.verify)
1821
/// before it can be used.
1922
class UnverifiedCertificateError extends AgentFetchError {
20-
UnverifiedCertificateError();
23+
UnverifiedCertificateError([this.reason = 'Certificate is not verified.']);
24+
25+
final String reason;
2126

2227
@override
23-
String toString() => 'Cannot lookup unverified certificate. '
24-
"Try to call 'verify()' again.";
28+
String toString() => reason;
2529
}
2630

2731
/// type HashTree =
@@ -47,24 +51,26 @@ enum NodeId {
4751
}
4852

4953
class Cert {
50-
const Cert({this.tree, this.signature, this.delegation});
54+
const Cert({
55+
required this.tree,
56+
required this.signature,
57+
required this.delegation,
58+
});
5159

5260
factory Cert.fromJson(Map json) {
5361
return Cert(
62+
tree: json['tree'],
63+
signature: (json['signature'] as Uint8Buffer).buffer.asUint8List(),
5464
delegation: json['delegation'] != null
5565
? CertDelegation.fromJson(
5666
Map<String, dynamic>.from(json['delegation']),
5767
)
5868
: null,
59-
signature: json['signature'] != null
60-
? (json['signature'] as Uint8Buffer).buffer.asUint8List()
61-
: null,
62-
tree: json['tree'],
6369
);
6470
}
6571

66-
final List? tree;
67-
final Uint8List? signature;
72+
final List tree;
73+
final Uint8List signature;
6874
final CertDelegation? delegation;
6975

7076
Map<String, dynamic> toJson() {
@@ -103,54 +109,61 @@ String hashTreeToString(List tree) {
103109

104110
class CertDelegation extends ReadStateResponse {
105111
const CertDelegation(
106-
this.subnetId,
107112
BinaryBlob certificate,
113+
this.subnetId,
108114
) : super(certificate: certificate);
109115

110116
factory CertDelegation.fromJson(Map<String, dynamic> json) {
111117
return CertDelegation(
112-
Uint8List.fromList(json['subnet_id'] as List<int>),
113118
json['certificate'] is Uint8List || json['certificate'] is Uint8Buffer
114119
? Uint8List.fromList(json['certificate'])
115120
: Uint8List.fromList([]),
121+
Uint8List.fromList(json['subnet_id'] as List<int>),
116122
);
117123
}
118124

119-
final Uint8List? subnetId;
125+
final Uint8List subnetId;
120126

121127
Map<String, dynamic> toJson() {
122-
return {'subnet_id': subnetId, 'certificate': certificate};
128+
return {
129+
'certificate': certificate,
130+
'subnet_id': subnetId,
131+
};
123132
}
124133
}
125134

126135
class Certificate {
127-
Certificate(
128-
BinaryBlob certificate,
129-
this._agent,
130-
) : cert = Cert.fromJson(cborDecode(certificate));
136+
Certificate({
137+
required BinaryBlob cert,
138+
required this.canisterId,
139+
this.rootKey,
140+
this.maxAgeInMinutes = 5,
141+
}) : assert(maxAgeInMinutes == null || maxAgeInMinutes <= 5),
142+
cert = Cert.fromJson(cborDecode(cert));
131143

132-
final Agent _agent;
133144
final Cert cert;
145+
final Principal canisterId;
146+
final BinaryBlob? rootKey;
147+
final int? maxAgeInMinutes;
148+
134149
bool verified = false;
135-
BinaryBlob? _rootKey;
136150

137-
Uint8List? lookupEx(List path) {
138-
checkState();
139-
return lookupPathEx(path, cert.tree!);
151+
Uint8List? lookup(List path) {
152+
return lookupPath(path, cert.tree);
140153
}
141154

142-
Uint8List? lookup(List path) {
143-
checkState();
144-
return lookupPath(path, cert.tree!);
155+
Uint8List? lookupEx(List path) {
156+
return lookupPathEx(path, cert.tree);
145157
}
146158

147159
Future<bool> verify() async {
148-
final rootHash = await reconstruct(cert.tree!);
160+
_verifyCertTime();
161+
final rootHash = await reconstruct(cert.tree);
149162
final derKey = await _checkDelegation(cert.delegation);
150-
final sig = cert.signature;
151163
final key = extractDER(derKey);
164+
final sig = cert.signature;
152165
final msg = u8aConcat([domainSep('ic-state-root'), rootHash]);
153-
final res = await _bls.blsVerify(key, sig!, msg);
166+
final res = await _bls.blsVerify(key, sig, msg);
154167
verified = res;
155168
return res;
156169
}
@@ -161,29 +174,80 @@ class Certificate {
161174
}
162175
}
163176

177+
void _verifyCertTime() {
178+
final timeLookup = lookupEx(['time']);
179+
if (timeLookup == null) {
180+
throw UnverifiedCertificateError('Certificate does not contain a time.');
181+
}
182+
final now = DateTime.now();
183+
final lebDecodedTime = lebDecode(BufferPipe(timeLookup));
184+
final time = DateTime.fromMicrosecondsSinceEpoch(
185+
(lebDecodedTime / BigInt.from(1000)).toInt(),
186+
);
187+
// Signed time is after 5 minutes from now.
188+
if (time.isAfter(now.add(const Duration(minutes: 5)))) {
189+
throw UnverifiedCertificateError(
190+
'Certificate is signed more than 5 minutes in the future.\n'
191+
'|-- Certificate time: ${time.toIso8601String()}\n'
192+
'|-- Current time: ${now.toIso8601String()}',
193+
);
194+
}
195+
// Signed time is before [maxAgeInMinutes] minutes.
196+
if (maxAgeInMinutes != null &&
197+
time.isBefore(now.subtract(Duration(minutes: maxAgeInMinutes!)))) {
198+
throw UnverifiedCertificateError(
199+
'Certificate is signed more than $maxAgeInMinutes minutes in the past.\n'
200+
'|-- Certificate time: ${time.toIso8601String()}\n'
201+
'|-- Current time: ${now.toIso8601String()}',
202+
);
203+
}
204+
}
205+
164206
Future<Uint8List> _checkDelegation(CertDelegation? d) async {
165207
if (d == null) {
166-
if (_rootKey == null) {
167-
if (_agent.rootKey != null) {
168-
_rootKey = _agent.rootKey;
169-
return Future.value(_rootKey);
170-
}
171-
throw StateError(
208+
if (rootKey == null) {
209+
throw UnverifiedCertificateError(
172210
'The rootKey is not exist. Try to call `fetchRootKey` again.',
173211
);
174212
}
175-
return Future.value(_rootKey);
213+
return Future.value(rootKey);
176214
}
177-
final Certificate cert = Certificate(d.certificate, _agent);
215+
final cert = Certificate(
216+
cert: d.certificate,
217+
canisterId: canisterId,
218+
rootKey: rootKey,
219+
maxAgeInMinutes: null, // Do not check max age for delegation certificates
220+
);
178221
if (!(await cert.verify())) {
179-
throw StateError('Fail to verify certificate.');
222+
throw UnverifiedCertificateError('Fail to verify certificate.');
223+
}
224+
225+
final canisterRangesLookup = cert.lookupEx(
226+
['subnet', d.subnetId, 'canister_ranges'],
227+
);
228+
if (canisterRangesLookup == null) {
229+
throw UnverifiedCertificateError(
230+
'Cannot find canister ranges for subnet 0x${d.subnetId.toHex()}.',
231+
);
232+
}
233+
final canisterRanges = cborDecode<List>(canisterRangesLookup).map((e) {
234+
final list = (e as List).cast<Uint8Buffer>();
235+
return (Principal(list.first.toU8a()), Principal(list.last.toU8a()));
236+
}).toList();
237+
if (!canisterRanges
238+
.any((range) => range.$1 <= canisterId && canisterId <= range.$2)) {
239+
throw UnverifiedCertificateError('Certificate is not authorized.');
180240
}
181241

182-
final lookup = cert.lookupEx(['subnet', d.subnetId, 'public_key']);
183-
if (lookup == null) {
184-
throw StateError('Cannot find subnet key for 0x${d.subnetId!.toHex()}.');
242+
final publicKeyLookup = cert.lookupEx(
243+
['subnet', d.subnetId, 'public_key'],
244+
);
245+
if (publicKeyLookup == null) {
246+
throw UnverifiedCertificateError(
247+
'Cannot find subnet key for 0x${d.subnetId.toHex()}.',
248+
);
185249
}
186-
return lookup;
250+
return publicKeyLookup;
187251
}
188252
}
189253

packages/agent_dart_base/lib/agent/polling/polling.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,26 @@ Future<BinaryBlob> pollForResponse(
2727
final path = [blobFromText('request_status'), requestId];
2828
final Certificate cert;
2929
if (overrideCertificate != null) {
30-
cert = Certificate(overrideCertificate, agent);
30+
cert = Certificate(
31+
cert: overrideCertificate,
32+
canisterId: canisterId,
33+
rootKey: agent.rootKey,
34+
);
3135
} else {
3236
final state = await agent.readState(
3337
canisterId,
3438
ReadStateOptions(paths: [path]),
3539
null,
3640
);
37-
cert = Certificate(state.certificate, agent);
41+
cert = Certificate(
42+
cert: state.certificate,
43+
canisterId: canisterId,
44+
rootKey: agent.rootKey,
45+
);
3846
}
3947
final verified = await cert.verify();
4048
if (!verified) {
41-
throw StateError('Fail to verify certificate.');
49+
throw UnverifiedCertificateError();
4250
}
4351

4452
final maybeBuf = cert.lookup([...path, blobFromText('status').buffer]);

packages/agent_dart_base/lib/agent/polling/strategy.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ PollStrategy defaultStrategy() {
1919
return chain([
2020
conditionalDelay(once(), 1000),
2121
backoff(1000, 1.2),
22-
timeout(5 * 60 * 1000),
22+
timeout(defaultExpireInDuration),
2323
]);
2424
}
2525

@@ -84,19 +84,19 @@ PollStrategy throttlePolling(int throttleMilliseconds) {
8484
};
8585
}
8686

87-
PollStrategy timeout(int milliseconds) {
88-
final end = DateTime.now().millisecondsSinceEpoch + milliseconds;
87+
PollStrategy timeout(Duration duration) {
88+
final end = DateTime.now().add(duration);
8989
return (
9090
Principal canisterId,
9191
RequestId requestId,
9292
RequestStatusResponseStatus status,
9393
) async {
94-
if (DateTime.now().millisecondsSinceEpoch > end) {
94+
if (DateTime.now().isAfter(end)) {
9595
throw TimeoutException(
96-
'Request timed out after $milliseconds milliseconds:\n'
96+
'Request timed out after $duration:\n'
9797
' Request ID: ${requestIdToHex(requestId)}\n'
9898
' Request status: $status\n',
99-
Duration(milliseconds: milliseconds),
99+
duration,
100100
);
101101
}
102102
};

packages/agent_dart_base/lib/identity/delegation.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,7 @@ class DelegationChain {
164164
DelegationChain? previous,
165165
List<Principal>? targets,
166166
}) async {
167-
expiration ??= DateTime.fromMillisecondsSinceEpoch(
168-
DateTime.now().millisecondsSinceEpoch + 15 * 60 * 1000,
169-
);
167+
expiration ??= DateTime.now().add(const Duration(minutes: 15));
170168
final delegation = await _createSingleDelegation(
171169
from,
172170
to,

packages/agent_dart_base/lib/principal/principal.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const _typeOpaque = 1;
1717

1818
final _emptySubAccount = Uint8List(32);
1919

20-
class Principal {
20+
class Principal implements Comparable<Principal> {
2121
const Principal(
2222
this._principal, {
2323
Uint8List? subAccount,
@@ -220,6 +220,20 @@ class Principal {
220220

221221
@override
222222
int get hashCode => Object.hash(_principal, subAccount);
223+
224+
@override
225+
int compareTo(Principal other) {
226+
for (int i = 0; i < _principal.length && i < other._principal.length; i++) {
227+
if (_principal[i] != other._principal[i]) {
228+
return _principal[i].compareTo(other._principal[i]);
229+
}
230+
}
231+
return _principal.length.compareTo(other._principal.length);
232+
}
233+
234+
bool operator <=(Principal other) => compareTo(other) <= 0;
235+
236+
bool operator >=(Principal other) => compareTo(other) >= 0;
223237
}
224238

225239
class CanisterId extends Principal {

0 commit comments

Comments
 (0)