@@ -5,23 +5,27 @@ import 'package:typed_data/typed_data.dart';
55
66import '../../utils/extension.dart' ;
77import '../../utils/u8a.dart' ;
8+ import '../principal/principal.dart' ;
89import 'agent/api.dart' ;
910import 'bls.dart' ;
1011import 'cbor.dart' ;
1112import 'errors.dart' ;
1213import 'request_id.dart' ;
1314import 'types.dart' ;
15+ import 'utils/buffer_pipe.dart' ;
16+ import 'utils/leb128.dart' ;
1417
1518final AgentBLS _bls = AgentBLS ();
1619
1720/// A certificate needs to be verified (using Certificate.prototype.verify)
1821/// before it can be used.
1922class 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
4953class 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
104110class 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
126135class 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
0 commit comments