From 3981879c1c5a82244081f7da1f42115a7ed643c1 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Tue, 15 Oct 2024 10:46:33 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9A=80=20Implement=20comparable=20Pri?= =?UTF-8?q?ncipal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_dart_base/lib/principal/principal.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/agent_dart_base/lib/principal/principal.dart b/packages/agent_dart_base/lib/principal/principal.dart index 97b8f16b..b56f2191 100644 --- a/packages/agent_dart_base/lib/principal/principal.dart +++ b/packages/agent_dart_base/lib/principal/principal.dart @@ -17,7 +17,7 @@ const _typeOpaque = 1; final _emptySubAccount = Uint8List(32); -class Principal { +class Principal implements Comparable { const Principal( this._principal, { Uint8List? subAccount, @@ -220,6 +220,20 @@ class Principal { @override int get hashCode => Object.hash(_principal, subAccount); + + @override + int compareTo(Principal other) { + for (int i = 0; i < _principal.length && i < other._principal.length; i++) { + if (_principal[i] != other._principal[i]) { + return _principal[i].compareTo(other._principal[i]); + } + } + return _principal.length.compareTo(other._principal.length); + } + + bool operator <=(Principal other) => compareTo(other) <= 0; + + bool operator >=(Principal other) => compareTo(other) >= 0; } class CanisterId extends Principal { From d185f6b3e355aa3bf160854c357e808d3fa9fb92 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Tue, 15 Oct 2024 10:47:34 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Verify=20canister?= =?UTF-8?q?=20ranges=20when=20checking=20certificate's=20delegation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/agent/certificate.dart | 50 ++++++++++++++----- .../lib/agent/polling/polling.dart | 2 +- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/packages/agent_dart_base/lib/agent/certificate.dart b/packages/agent_dart_base/lib/agent/certificate.dart index 330bb17d..27836724 100644 --- a/packages/agent_dart_base/lib/agent/certificate.dart +++ b/packages/agent_dart_base/lib/agent/certificate.dart @@ -5,6 +5,7 @@ import 'package:typed_data/typed_data.dart'; import '../../utils/extension.dart'; import '../../utils/u8a.dart'; +import '../principal/principal.dart'; import 'agent/api.dart'; import 'bls.dart'; import 'cbor.dart'; @@ -103,23 +104,26 @@ String hashTreeToString(List tree) { class CertDelegation extends ReadStateResponse { const CertDelegation( - this.subnetId, BinaryBlob certificate, + this.subnetId, ) : super(certificate: certificate); factory CertDelegation.fromJson(Map json) { return CertDelegation( - Uint8List.fromList(json['subnet_id'] as List), json['certificate'] is Uint8List || json['certificate'] is Uint8Buffer ? Uint8List.fromList(json['certificate']) : Uint8List.fromList([]), + Uint8List.fromList(json['subnet_id'] as List), ); } - final Uint8List? subnetId; + final Uint8List subnetId; Map toJson() { - return {'subnet_id': subnetId, 'certificate': certificate}; + return { + 'certificate': certificate, + 'subnet_id': subnetId, + }; } } @@ -144,9 +148,9 @@ class Certificate { return lookupPath(path, cert.tree!); } - Future verify() async { + Future verify(Principal canisterId) async { final rootHash = await reconstruct(cert.tree!); - final derKey = await _checkDelegation(cert.delegation); + final derKey = await _checkDelegation(cert.delegation, canisterId); final sig = cert.signature; final key = extractDER(derKey); final msg = u8aConcat([domainSep('ic-state-root'), rootHash]); @@ -161,7 +165,10 @@ class Certificate { } } - Future _checkDelegation(CertDelegation? d) async { + Future _checkDelegation( + CertDelegation? d, + Principal canisterId, + ) async { if (d == null) { if (_rootKey == null) { if (_agent.rootKey != null) { @@ -175,15 +182,34 @@ class Certificate { return Future.value(_rootKey); } final Certificate cert = Certificate(d.certificate, _agent); - if (!(await cert.verify())) { + if (!(await cert.verify(canisterId))) { throw StateError('Fail to verify certificate.'); } - final lookup = cert.lookupEx(['subnet', d.subnetId, 'public_key']); - if (lookup == null) { - throw StateError('Cannot find subnet key for 0x${d.subnetId!.toHex()}.'); + final canisterRangesLookup = cert.lookupEx( + ['subnet', d.subnetId, 'canister_ranges'], + ); + if (canisterRangesLookup == null) { + throw StateError( + 'Cannot find canister ranges for subnet 0x${d.subnetId.toHex()}.', + ); + } + final canisterRanges = cborDecode(canisterRangesLookup).map((e) { + final list = (e as List).cast(); + return (Principal(list.first.toU8a()), Principal(list.last.toU8a())); + }).toList(); + if (!canisterRanges + .any((range) => range.$1 <= canisterId && canisterId <= range.$2)) { + throw StateError('Certificate is not authorized.'); + } + + final publicKeyLookup = cert.lookupEx( + ['subnet', d.subnetId, 'public_key'], + ); + if (publicKeyLookup == null) { + throw StateError('Cannot find subnet key for 0x${d.subnetId.toHex()}.'); } - return lookup; + return publicKeyLookup; } } diff --git a/packages/agent_dart_base/lib/agent/polling/polling.dart b/packages/agent_dart_base/lib/agent/polling/polling.dart index 45ce9fbb..ad1337e2 100644 --- a/packages/agent_dart_base/lib/agent/polling/polling.dart +++ b/packages/agent_dart_base/lib/agent/polling/polling.dart @@ -36,7 +36,7 @@ Future pollForResponse( ); cert = Certificate(state.certificate, agent); } - final verified = await cert.verify(); + final verified = await cert.verify(canisterId); if (!verified) { throw StateError('Fail to verify certificate.'); } From b2be7ffb3d34bef7ee53592b00563dfa0ee18e9b Mon Sep 17 00:00:00 2001 From: Alex Li Date: Tue, 15 Oct 2024 15:24:54 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reorg=20expire=20durat?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_dart_base/lib/agent/agent/http/index.dart | 7 ++++++- .../agent_dart_base/lib/agent/polling/strategy.dart | 12 ++++++------ .../agent_dart_base/lib/identity/delegation.dart | 4 +--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/agent_dart_base/lib/agent/agent/http/index.dart b/packages/agent_dart_base/lib/agent/agent/http/index.dart index 11b3b6ec..e9a0714b 100644 --- a/packages/agent_dart_base/lib/agent/agent/http/index.dart +++ b/packages/agent_dart_base/lib/agent/agent/http/index.dart @@ -39,8 +39,13 @@ Future withRetry( } } +/// Most of the timeouts will happen in 5 minutes. +const defaultExpireInMinutes = 5; +const defaultExpireInDuration = Duration(minutes: defaultExpireInMinutes); + /// Default delta for ingress expiry is 5 minutes. -const _defaultIngressExpiryDeltaInMilliseconds = 5 * 60 * 1000; +const _defaultIngressExpiryDeltaInMilliseconds = + defaultExpireInMinutes * 60 * 1000; /// Root public key for the IC, encoded as hex const _icRootKey = '308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7' diff --git a/packages/agent_dart_base/lib/agent/polling/strategy.dart b/packages/agent_dart_base/lib/agent/polling/strategy.dart index eb391d60..9a070bde 100644 --- a/packages/agent_dart_base/lib/agent/polling/strategy.dart +++ b/packages/agent_dart_base/lib/agent/polling/strategy.dart @@ -19,7 +19,7 @@ PollStrategy defaultStrategy() { return chain([ conditionalDelay(once(), 1000), backoff(1000, 1.2), - timeout(5 * 60 * 1000), + timeout(defaultExpireInDuration), ]); } @@ -84,19 +84,19 @@ PollStrategy throttlePolling(int throttleMilliseconds) { }; } -PollStrategy timeout(int milliseconds) { - final end = DateTime.now().millisecondsSinceEpoch + milliseconds; +PollStrategy timeout(Duration duration) { + final end = DateTime.now().add(duration); return ( Principal canisterId, RequestId requestId, RequestStatusResponseStatus status, ) async { - if (DateTime.now().millisecondsSinceEpoch > end) { + if (DateTime.now().isAfter(end)) { throw TimeoutException( - 'Request timed out after $milliseconds milliseconds:\n' + 'Request timed out after $duration:\n' ' Request ID: ${requestIdToHex(requestId)}\n' ' Request status: $status\n', - Duration(milliseconds: milliseconds), + duration, ); } }; diff --git a/packages/agent_dart_base/lib/identity/delegation.dart b/packages/agent_dart_base/lib/identity/delegation.dart index 7cd2eab1..58f2b7e9 100644 --- a/packages/agent_dart_base/lib/identity/delegation.dart +++ b/packages/agent_dart_base/lib/identity/delegation.dart @@ -164,9 +164,7 @@ class DelegationChain { DelegationChain? previous, List? targets, }) async { - expiration ??= DateTime.fromMillisecondsSinceEpoch( - DateTime.now().millisecondsSinceEpoch + 15 * 60 * 1000, - ); + expiration ??= DateTime.now().add(const Duration(minutes: 15)); final delegation = await _createSingleDelegation( from, to, From c18a825bf0520f00ae676da7c736d6208742f79d Mon Sep 17 00:00:00 2001 From: Alex Li Date: Tue, 15 Oct 2024 15:25:38 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reorg=20`Certificate`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/agent/certificate.dart | 93 ++++++++++--------- .../lib/agent/polling/polling.dart | 16 +++- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/packages/agent_dart_base/lib/agent/certificate.dart b/packages/agent_dart_base/lib/agent/certificate.dart index 27836724..f8323139 100644 --- a/packages/agent_dart_base/lib/agent/certificate.dart +++ b/packages/agent_dart_base/lib/agent/certificate.dart @@ -18,11 +18,12 @@ final AgentBLS _bls = AgentBLS(); /// A certificate needs to be verified (using Certificate.prototype.verify) /// before it can be used. class UnverifiedCertificateError extends AgentFetchError { - UnverifiedCertificateError(); + UnverifiedCertificateError([this.reason = 'Certificate is not verified.']); + + final String reason; @override - String toString() => 'Cannot lookup unverified certificate. ' - "Try to call 'verify()' again."; + String toString() => reason; } /// type HashTree = @@ -48,24 +49,26 @@ enum NodeId { } class Cert { - const Cert({this.tree, this.signature, this.delegation}); + const Cert({ + required this.tree, + required this.signature, + required this.delegation, + }); factory Cert.fromJson(Map json) { return Cert( + tree: json['tree'], + signature: (json['signature'] as Uint8Buffer).buffer.asUint8List(), delegation: json['delegation'] != null ? CertDelegation.fromJson( Map.from(json['delegation']), ) : null, - signature: json['signature'] != null - ? (json['signature'] as Uint8Buffer).buffer.asUint8List() - : null, - tree: json['tree'], ); } - final List? tree; - final Uint8List? signature; + final List tree; + final Uint8List signature; final CertDelegation? delegation; Map toJson() { @@ -128,33 +131,36 @@ class CertDelegation extends ReadStateResponse { } class Certificate { - Certificate( - BinaryBlob certificate, - this._agent, - ) : cert = Cert.fromJson(cborDecode(certificate)); + Certificate({ + required BinaryBlob cert, + required this.canisterId, + this.rootKey, + this.maxAgeInMinutes = 5, + }) : assert(maxAgeInMinutes <= 5), + cert = Cert.fromJson(cborDecode(cert)); - final Agent _agent; final Cert cert; + final Principal canisterId; + final BinaryBlob? rootKey; + final int maxAgeInMinutes; + bool verified = false; - BinaryBlob? _rootKey; - Uint8List? lookupEx(List path) { - checkState(); - return lookupPathEx(path, cert.tree!); + Uint8List? lookup(List path) { + return lookupPath(path, cert.tree); } - Uint8List? lookup(List path) { - checkState(); - return lookupPath(path, cert.tree!); + Uint8List? lookupEx(List path) { + return lookupPathEx(path, cert.tree); } - Future verify(Principal canisterId) async { - final rootHash = await reconstruct(cert.tree!); - final derKey = await _checkDelegation(cert.delegation, canisterId); - final sig = cert.signature; + Future verify() async { + final rootHash = await reconstruct(cert.tree); + final derKey = await _checkDelegation(cert.delegation); final key = extractDER(derKey); + final sig = cert.signature; final msg = u8aConcat([domainSep('ic-state-root'), rootHash]); - final res = await _bls.blsVerify(key, sig!, msg); + final res = await _bls.blsVerify(key, sig, msg); verified = res; return res; } @@ -165,32 +171,29 @@ class Certificate { } } - Future _checkDelegation( - CertDelegation? d, - Principal canisterId, - ) async { + Future _checkDelegation(CertDelegation? d) async { if (d == null) { - if (_rootKey == null) { - if (_agent.rootKey != null) { - _rootKey = _agent.rootKey; - return Future.value(_rootKey); - } - throw StateError( + if (rootKey == null) { + throw UnverifiedCertificateError( 'The rootKey is not exist. Try to call `fetchRootKey` again.', ); } - return Future.value(_rootKey); + return Future.value(rootKey); } - final Certificate cert = Certificate(d.certificate, _agent); - if (!(await cert.verify(canisterId))) { - throw StateError('Fail to verify certificate.'); + final cert = Certificate( + cert: d.certificate, + canisterId: canisterId, + rootKey: rootKey, + ); + if (!(await cert.verify())) { + throw UnverifiedCertificateError('Fail to verify certificate.'); } final canisterRangesLookup = cert.lookupEx( ['subnet', d.subnetId, 'canister_ranges'], ); if (canisterRangesLookup == null) { - throw StateError( + throw UnverifiedCertificateError( 'Cannot find canister ranges for subnet 0x${d.subnetId.toHex()}.', ); } @@ -200,14 +203,16 @@ class Certificate { }).toList(); if (!canisterRanges .any((range) => range.$1 <= canisterId && canisterId <= range.$2)) { - throw StateError('Certificate is not authorized.'); + throw UnverifiedCertificateError('Certificate is not authorized.'); } final publicKeyLookup = cert.lookupEx( ['subnet', d.subnetId, 'public_key'], ); if (publicKeyLookup == null) { - throw StateError('Cannot find subnet key for 0x${d.subnetId.toHex()}.'); + throw UnverifiedCertificateError( + 'Cannot find subnet key for 0x${d.subnetId.toHex()}.', + ); } return publicKeyLookup; } diff --git a/packages/agent_dart_base/lib/agent/polling/polling.dart b/packages/agent_dart_base/lib/agent/polling/polling.dart index ad1337e2..3de17e35 100644 --- a/packages/agent_dart_base/lib/agent/polling/polling.dart +++ b/packages/agent_dart_base/lib/agent/polling/polling.dart @@ -27,18 +27,26 @@ Future pollForResponse( final path = [blobFromText('request_status'), requestId]; final Certificate cert; if (overrideCertificate != null) { - cert = Certificate(overrideCertificate, agent); + cert = Certificate( + cert: overrideCertificate, + canisterId: canisterId, + rootKey: agent.rootKey, + ); } else { final state = await agent.readState( canisterId, ReadStateOptions(paths: [path]), null, ); - cert = Certificate(state.certificate, agent); + cert = Certificate( + cert: state.certificate, + canisterId: canisterId, + rootKey: agent.rootKey, + ); } - final verified = await cert.verify(canisterId); + final verified = await cert.verify(); if (!verified) { - throw StateError('Fail to verify certificate.'); + throw UnverifiedCertificateError(); } final maybeBuf = cert.lookup([...path, blobFromText('status').buffer]); From b3d3a9e6a90ba4f16986409e4d9c56e9052173af Mon Sep 17 00:00:00 2001 From: Alex Li Date: Tue, 15 Oct 2024 15:42:45 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Verify=20certificat?= =?UTF-8?q?e=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/agent/certificate.dart | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/agent_dart_base/lib/agent/certificate.dart b/packages/agent_dart_base/lib/agent/certificate.dart index f8323139..044defae 100644 --- a/packages/agent_dart_base/lib/agent/certificate.dart +++ b/packages/agent_dart_base/lib/agent/certificate.dart @@ -12,6 +12,8 @@ import 'cbor.dart'; import 'errors.dart'; import 'request_id.dart'; import 'types.dart'; +import 'utils/buffer_pipe.dart'; +import 'utils/leb128.dart'; final AgentBLS _bls = AgentBLS(); @@ -136,13 +138,13 @@ class Certificate { required this.canisterId, this.rootKey, this.maxAgeInMinutes = 5, - }) : assert(maxAgeInMinutes <= 5), + }) : assert(maxAgeInMinutes == null || maxAgeInMinutes <= 5), cert = Cert.fromJson(cborDecode(cert)); final Cert cert; final Principal canisterId; final BinaryBlob? rootKey; - final int maxAgeInMinutes; + final int? maxAgeInMinutes; bool verified = false; @@ -155,6 +157,7 @@ class Certificate { } Future verify() async { + _verifyCertTime(); final rootHash = await reconstruct(cert.tree); final derKey = await _checkDelegation(cert.delegation); final key = extractDER(derKey); @@ -171,6 +174,35 @@ class Certificate { } } + void _verifyCertTime() { + final timeLookup = lookupEx(['time']); + if (timeLookup == null) { + throw UnverifiedCertificateError('Certificate does not contain a time.'); + } + final now = DateTime.now(); + final lebDecodedTime = lebDecode(BufferPipe(timeLookup)); + final time = DateTime.fromMicrosecondsSinceEpoch( + (lebDecodedTime / BigInt.from(1000)).toInt(), + ); + // Signed time is after 5 minutes from now. + if (time.isAfter(now.add(const Duration(minutes: 5)))) { + throw UnverifiedCertificateError( + 'Certificate is signed more than 5 minutes in the future.\n' + '|-- Certificate time: ${time.toIso8601String()}\n' + '|-- Current time: ${now.toIso8601String()}', + ); + } + // Signed time is before [maxAgeInMinutes] minutes. + if (maxAgeInMinutes != null && + time.isBefore(now.subtract(Duration(minutes: maxAgeInMinutes!)))) { + throw UnverifiedCertificateError( + 'Certificate is signed more than $maxAgeInMinutes minutes in the past.\n' + '|-- Certificate time: ${time.toIso8601String()}\n' + '|-- Current time: ${now.toIso8601String()}', + ); + } + } + Future _checkDelegation(CertDelegation? d) async { if (d == null) { if (rootKey == null) { @@ -184,6 +216,7 @@ class Certificate { cert: d.certificate, canisterId: canisterId, rootKey: rootKey, + maxAgeInMinutes: null, // Do not check max age for delegation certificates ); if (!(await cert.verify())) { throw UnverifiedCertificateError('Fail to verify certificate.');