@@ -5,23 +5,27 @@ import 'package:typed_data/typed_data.dart';
5
5
6
6
import '../../utils/extension.dart' ;
7
7
import '../../utils/u8a.dart' ;
8
+ import '../principal/principal.dart' ;
8
9
import 'agent/api.dart' ;
9
10
import 'bls.dart' ;
10
11
import 'cbor.dart' ;
11
12
import 'errors.dart' ;
12
13
import 'request_id.dart' ;
13
14
import 'types.dart' ;
15
+ import 'utils/buffer_pipe.dart' ;
16
+ import 'utils/leb128.dart' ;
14
17
15
18
final AgentBLS _bls = AgentBLS ();
16
19
17
20
/// A certificate needs to be verified (using Certificate.prototype.verify)
18
21
/// before it can be used.
19
22
class UnverifiedCertificateError extends AgentFetchError {
20
- UnverifiedCertificateError ();
23
+ UnverifiedCertificateError ([this .reason = 'Certificate is not verified.' ]);
24
+
25
+ final String reason;
21
26
22
27
@override
23
- String toString () => 'Cannot lookup unverified certificate. '
24
- "Try to call 'verify()' again." ;
28
+ String toString () => reason;
25
29
}
26
30
27
31
/// type HashTree =
@@ -47,24 +51,26 @@ enum NodeId {
47
51
}
48
52
49
53
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
+ });
51
59
52
60
factory Cert .fromJson (Map json) {
53
61
return Cert (
62
+ tree: json['tree' ],
63
+ signature: (json['signature' ] as Uint8Buffer ).buffer.asUint8List (),
54
64
delegation: json['delegation' ] != null
55
65
? CertDelegation .fromJson (
56
66
Map <String , dynamic >.from (json['delegation' ]),
57
67
)
58
68
: null ,
59
- signature: json['signature' ] != null
60
- ? (json['signature' ] as Uint8Buffer ).buffer.asUint8List ()
61
- : null ,
62
- tree: json['tree' ],
63
69
);
64
70
}
65
71
66
- final List ? tree;
67
- final Uint8List ? signature;
72
+ final List tree;
73
+ final Uint8List signature;
68
74
final CertDelegation ? delegation;
69
75
70
76
Map <String , dynamic > toJson () {
@@ -103,54 +109,61 @@ String hashTreeToString(List tree) {
103
109
104
110
class CertDelegation extends ReadStateResponse {
105
111
const CertDelegation (
106
- this .subnetId,
107
112
BinaryBlob certificate,
113
+ this .subnetId,
108
114
) : super (certificate: certificate);
109
115
110
116
factory CertDelegation .fromJson (Map <String , dynamic > json) {
111
117
return CertDelegation (
112
- Uint8List .fromList (json['subnet_id' ] as List <int >),
113
118
json['certificate' ] is Uint8List || json['certificate' ] is Uint8Buffer
114
119
? Uint8List .fromList (json['certificate' ])
115
120
: Uint8List .fromList ([]),
121
+ Uint8List .fromList (json['subnet_id' ] as List <int >),
116
122
);
117
123
}
118
124
119
- final Uint8List ? subnetId;
125
+ final Uint8List subnetId;
120
126
121
127
Map <String , dynamic > toJson () {
122
- return {'subnet_id' : subnetId, 'certificate' : certificate};
128
+ return {
129
+ 'certificate' : certificate,
130
+ 'subnet_id' : subnetId,
131
+ };
123
132
}
124
133
}
125
134
126
135
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));
131
143
132
- final Agent _agent;
133
144
final Cert cert;
145
+ final Principal canisterId;
146
+ final BinaryBlob ? rootKey;
147
+ final int ? maxAgeInMinutes;
148
+
134
149
bool verified = false ;
135
- BinaryBlob ? _rootKey;
136
150
137
- Uint8List ? lookupEx (List path) {
138
- checkState ();
139
- return lookupPathEx (path, cert.tree! );
151
+ Uint8List ? lookup (List path) {
152
+ return lookupPath (path, cert.tree);
140
153
}
141
154
142
- Uint8List ? lookup (List path) {
143
- checkState ();
144
- return lookupPath (path, cert.tree! );
155
+ Uint8List ? lookupEx (List path) {
156
+ return lookupPathEx (path, cert.tree);
145
157
}
146
158
147
159
Future <bool > verify () async {
148
- final rootHash = await reconstruct (cert.tree! );
160
+ _verifyCertTime ();
161
+ final rootHash = await reconstruct (cert.tree);
149
162
final derKey = await _checkDelegation (cert.delegation);
150
- final sig = cert.signature;
151
163
final key = extractDER (derKey);
164
+ final sig = cert.signature;
152
165
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);
154
167
verified = res;
155
168
return res;
156
169
}
@@ -161,29 +174,80 @@ class Certificate {
161
174
}
162
175
}
163
176
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
+
164
206
Future <Uint8List > _checkDelegation (CertDelegation ? d) async {
165
207
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 (
172
210
'The rootKey is not exist. Try to call `fetchRootKey` again.' ,
173
211
);
174
212
}
175
- return Future .value (_rootKey );
213
+ return Future .value (rootKey );
176
214
}
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
+ );
178
221
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.' );
180
240
}
181
241
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
+ );
185
249
}
186
- return lookup ;
250
+ return publicKeyLookup ;
187
251
}
188
252
}
189
253
0 commit comments