Skip to content

Commit

Permalink
Merge commit from fork
Browse files Browse the repository at this point in the history
🔒️ Implement certificate's time and canister ranges checks
  • Loading branch information
AlexV525 authored Oct 15, 2024
2 parents 47b2844 + c6668fc commit 0d20068
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 56 deletions.
7 changes: 6 additions & 1 deletion packages/agent_dart_base/lib/agent/agent/http/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ Future<T> withRetry<T>(
}
}

/// 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'
Expand Down
148 changes: 106 additions & 42 deletions packages/agent_dart_base/lib/agent/certificate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@ 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';
import 'errors.dart';
import 'request_id.dart';
import 'types.dart';
import 'utils/buffer_pipe.dart';
import 'utils/leb128.dart';

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 =
Expand All @@ -47,24 +51,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<String, dynamic>.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<String, dynamic> toJson() {
Expand Down Expand Up @@ -103,54 +109,61 @@ String hashTreeToString(List tree) {

class CertDelegation extends ReadStateResponse {
const CertDelegation(
this.subnetId,
BinaryBlob certificate,
this.subnetId,
) : super(certificate: certificate);

factory CertDelegation.fromJson(Map<String, dynamic> json) {
return CertDelegation(
Uint8List.fromList(json['subnet_id'] as List<int>),
json['certificate'] is Uint8List || json['certificate'] is Uint8Buffer
? Uint8List.fromList(json['certificate'])
: Uint8List.fromList([]),
Uint8List.fromList(json['subnet_id'] as List<int>),
);
}

final Uint8List? subnetId;
final Uint8List subnetId;

Map<String, dynamic> toJson() {
return {'subnet_id': subnetId, 'certificate': certificate};
return {
'certificate': certificate,
'subnet_id': subnetId,
};
}
}

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 == null || 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<bool> verify() async {
final rootHash = await reconstruct(cert.tree!);
_verifyCertTime();
final rootHash = await reconstruct(cert.tree);
final derKey = await _checkDelegation(cert.delegation);
final sig = cert.signature;
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;
}
Expand All @@ -161,29 +174,80 @@ 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<Uint8List> _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);
final cert = Certificate(
cert: d.certificate,
canisterId: canisterId,
rootKey: rootKey,
maxAgeInMinutes: null, // Do not check max age for delegation certificates
);
if (!(await cert.verify())) {
throw StateError('Fail to verify certificate.');
throw UnverifiedCertificateError('Fail to verify certificate.');
}

final canisterRangesLookup = cert.lookupEx(
['subnet', d.subnetId, 'canister_ranges'],
);
if (canisterRangesLookup == null) {
throw UnverifiedCertificateError(
'Cannot find canister ranges for subnet 0x${d.subnetId.toHex()}.',
);
}
final canisterRanges = cborDecode<List>(canisterRangesLookup).map((e) {
final list = (e as List).cast<Uint8Buffer>();
return (Principal(list.first.toU8a()), Principal(list.last.toU8a()));
}).toList();
if (!canisterRanges
.any((range) => range.$1 <= canisterId && canisterId <= range.$2)) {
throw UnverifiedCertificateError('Certificate is not authorized.');
}

final lookup = cert.lookupEx(['subnet', d.subnetId, 'public_key']);
if (lookup == null) {
throw StateError('Cannot find subnet key for 0x${d.subnetId!.toHex()}.');
final publicKeyLookup = cert.lookupEx(
['subnet', d.subnetId, 'public_key'],
);
if (publicKeyLookup == null) {
throw UnverifiedCertificateError(
'Cannot find subnet key for 0x${d.subnetId.toHex()}.',
);
}
return lookup;
return publicKeyLookup;
}
}

Expand Down
14 changes: 11 additions & 3 deletions packages/agent_dart_base/lib/agent/polling/polling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,26 @@ Future<BinaryBlob> 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();
if (!verified) {
throw StateError('Fail to verify certificate.');
throw UnverifiedCertificateError();
}

final maybeBuf = cert.lookup([...path, blobFromText('status').buffer]);
Expand Down
12 changes: 6 additions & 6 deletions packages/agent_dart_base/lib/agent/polling/strategy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ PollStrategy defaultStrategy() {
return chain([
conditionalDelay(once(), 1000),
backoff(1000, 1.2),
timeout(5 * 60 * 1000),
timeout(defaultExpireInDuration),
]);
}

Expand Down Expand Up @@ -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,
);
}
};
Expand Down
4 changes: 1 addition & 3 deletions packages/agent_dart_base/lib/identity/delegation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ class DelegationChain {
DelegationChain? previous,
List<Principal>? 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,
Expand Down
16 changes: 15 additions & 1 deletion packages/agent_dart_base/lib/principal/principal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const _typeOpaque = 1;

final _emptySubAccount = Uint8List(32);

class Principal {
class Principal implements Comparable<Principal> {
const Principal(
this._principal, {
Uint8List? subAccount,
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 0d20068

Please sign in to comment.