Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: skip range check when the certificate comes from the management canister #945

Merged
merged 8 commits into from
Oct 22, 2024
14 changes: 14 additions & 0 deletions packages/agent/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
QueryResponseStatus,
ReplicaRejectCode,
SubmitResponse,
v2ResponseBody,
v3ResponseBody,
} from './agent';
import { AgentError } from './errors';
Expand Down Expand Up @@ -581,7 +582,20 @@ function _createActorMethod(
);
}
}
} else if (!response.ok || response.body /* IC-1462 */) {
// handle v2 response errors by throwing an UpdateCallRejectedError object
const { reject_code, reject_message, error_code } = response.body as v2ResponseBody;
throw new UpdateCallRejectedError(
cid,
methodName,
requestId,
response,
reject_code,
reject_message,
error_code,
);
}

// Fall back to polling if we receive an Accepted response code
if (response.status === 202) {
const pollStrategy = pollingStrategyFactory();
Expand Down
75 changes: 75 additions & 0 deletions packages/agent/src/agent/http/__snapshots__/http.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`it should handle calls against the ic-management canister that succeed 1`] = `
{
"cycles": 3092219247033n,
"idle_cycles_burned_per_day": 1808810n,
"memory_size": 2301012n,
"module_hash": [
Uint8Array [
254,
155,
232,
199,
49,
146,
52,
52,
57,
201,
131,
209,
77,
162,
243,
122,
89,
50,
105,
40,
93,
49,
15,
210,
193,
29,
73,
112,
229,
241,
110,
182,
],
],
"query_stats": {
"num_calls_total": 0n,
"num_instructions_total": 0n,
"request_payload_bytes_total": 0n,
"response_payload_bytes_total": 0n,
},
"reserved_cycles": 0n,
"settings": {
"compute_allocation": 0n,
"controllers": [
{
"__principal__": "2vxsx-fae",
},
{
"__principal__": "bnz7o-iuaaa-aaaaa-qaaaa-cai",
},
{
"__principal__": "jhnlf-yu2dz-v7beb-c77gl-76tj7-shaqo-5qfvi-htvel-gzamb-bvzx6-yqe",
},
],
"freezing_threshold": 2592000n,
"log_visibility": {
"controllers": null,
},
"memory_allocation": 0n,
"reserved_cycles_limit": 5000000000000n,
"wasm_memory_limit": 0n,
},
"status": {
"running": null,
},
}
`;

exports[`retry failures should succeed after multiple failures within the configured limit 1`] = `
{
"requestDetails": undefined,
Expand Down
145 changes: 144 additions & 1 deletion packages/agent/src/agent/http/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import { Principal } from '@dfinity/principal';
import { requestIdOf } from '../../request_id';

import { JSDOM } from 'jsdom';
import { Actor, AnonymousIdentity, SignIdentity, toHex } from '../..';
import {
Actor,
AnonymousIdentity,
fromHex,
getManagementCanister,
SignIdentity,
toHex,
} from '../..';
import { Ed25519KeyIdentity } from '@dfinity/identity';
import { AgentError } from '../../errors';
import { AgentHTTPResponseError } from './errors';
Expand Down Expand Up @@ -813,3 +820,139 @@ test('it should log errors to console if the option is set', async () => {
await agent.syncTime();
});

test('it should handle calls against the ic-management canister that are rejected', async () => {
const identity = new AnonymousIdentity();
identity.getPrincipal().toString();

// Response generated by calling a locally deployed replica of the management canister, cloned using fetchCloner
const mockResponse = {
headers: [
['access-control-allow-origin', '*'],
['content-length', '178'],
['content-type', 'application/cbor'],
['date', 'Mon, 21 Oct 2024 23:35:59 GMT'],
],
ok: true,
status: 200,
statusText: 'OK',
body: 'd9d9f7a46673746174757378186e6f6e5f7265706c6963617465645f72656a656374696f6e6a6572726f725f636f6465664943303531326b72656a6563745f636f6465056e72656a6563745f6d657373616765785d4f6e6c7920636f6e74726f6c6c657273206f662063616e697374657220626b797a322d666d6161612d61616161612d71616161712d6361692063616e2063616c6c2069633030206d6574686f642063616e69737465725f737461747573',
now: 1729553760128,
};

// Mock the fetch implementation, resolving a pre-calculated response
const mockFetch: jest.Mock = jest.fn(() => {
return Promise.resolve({
...mockResponse,
body: fromHex(mockResponse.body),
arrayBuffer: async () => fromHex(mockResponse.body),
});
});

// Mock time so certificates can be accurately decoded
jest.useFakeTimers();
jest.setSystemTime(mockResponse.now);

const agent = await HttpAgent.createSync({
identity,
fetch: mockFetch,
host: 'http://localhost:4943',
});

// Use management canister call
const management = getManagementCanister({ agent });

// Call snapshot was made when the test canister was not authorized to be called by the anonymous identity. It should reject
expect(
management.canister_status({
canister_id: Principal.from('bkyz2-fmaaa-aaaaa-qaaaq-cai'),
}),
).rejects.toThrow(
'Only controllers of canister bkyz2-fmaaa-aaaaa-qaaaq-cai can call ic00 method canister_status',
);
});
test('it should handle calls against the ic-management canister that succeed', async () => {
const identity = new AnonymousIdentity();

// Response generated by calling a locally deployed replica of the management canister, cloned using fetchCloner
const mockResponse = {
headers: [
['access-control-allow-origin', '*'],
['content-length', '761'],
['content-type', 'application/cbor'],
['date', 'Tue, 22 Oct 2024 22:19:07 GMT'],
],
ok: true,
status: 200,
statusText: 'OK',
body: 'd9d9f7a266737461747573677265706c6965646b63657274696669636174655902d7d9d9f7a26474726565830183018204582012dbb02955bd3e2987bbba491230b2bb4a593feb02b5bb2d08f5f861afa9cec28301820458202b60693266aeec370be9f54508af493f4dd740086476054c862fe5af17ab15c183024e726571756573745f73746174757383018301820458204bebdfa0327978bfb109f0e14b35e8d368bb62114628ae547386162e9ee3dad883025820cf1cd57f39dfbb40ca1c816c71407c8b1b2edfb5a632676c7917dc4aa8641c5283018302457265706c7982035901684449444c0a6c0b9cb1fa2568b2ceef2f01c0cff2717d9cbab69c0202ffdb81f7037d8daacd94087de3f9f5d90805e8fc8cec0908b0e4d2970a7d81cfaef40a0984aaa89e0f7d6b038da4879b047ff496e4910b7fffdba5db0e7f6d036c020004017d6d7b6c089cb1fa2568c0cff2717dd7e09b90020680ad988a047dedd9c8c90707f8e287cc0c7ddeebb5a90e7da882acc60f7d6d686b02d7e09b90027fa981ceb7067f6c04c1f8dc83037d83cac6e9057da1d0b8af0a7d8fd0cfd00f7d6e040100011100001945cd0f5904e6ce2e5ac91900fb0102809a9e01010100b9f384b5ff59d4b88c01b9f384b5ff59011100001945cd0f5904e6ce2e5ac91900fb01809a9e0103010104010a80000000001000000101011d9a1e6bf09022ffccbffa69fc8e083bb02d5079d48b3640c086b9bfb10280a0e5b9c291010000000000000000aab36e0120fe9be8c73192343439c983d14da2f37a593269285d310fd2c11d4970e5f16eb6008302467374617475738203477265706c69656482045820daeffcc5dadc3aca94e0dc470e429ad4e3bc08517b5776f6a71e7e6982883bef8301820458208e6c6a7c4ba444475de4f4cd2d6df9501873d3290693060faf92e6dc528ee08083024474696d65820349a88dd3f9dacbb98018697369676e6174757265583088040a8228ef3f428c61918c5fb356e74b2ab07aa19f960edbb1fdfcbbc115e35f2e6c3f33b5cf4752799619e67e2b22',
now: 1729635546372,
};

// Mock the fetch implementation, resolving a pre-calculated response
const mockFetch: jest.Mock = jest.fn(() => {
return Promise.resolve({
...mockResponse,
body: fromHex(mockResponse.body),
arrayBuffer: async () => fromHex(mockResponse.body),
});
});

// Mock time so certificates can be accurately decoded
jest.useFakeTimers();
jest.setSystemTime(mockResponse.now);

// Pass in rootKey from replica (used because test was written using local replica)
const agent = await HttpAgent.createSync({
identity,
fetch: mockFetch,
host: 'http://localhost:4943',
rootKey: fromHex(
'308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361008be882f1985cccb53fd551571a42818014835ed8f8a27767669b67dd4a836eb0d62b327e3368a80615b0e4f472c73f7917c036dc9317dcb64b319a1efa43dd7c656225c061de359db6fdf7033ac1bff24c944c145e46ebdce2093680b6209a13',
),
});

// Use management canister call
const management = getManagementCanister({ agent });

// Important - override nonce when making request to ensure reproducible result
(Actor.agentOf(management) as HttpAgent).addTransform('update', async args => {
args.body.nonce = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]) as Nonce;
return args;
});

// Call snapshot was made after the test canister was authorized to be called by the anonymous identity. It should resolve the status
const status = await management.canister_status({
canister_id: Principal.from('bkyz2-fmaaa-aaaaa-qaaaq-cai'),
});

expect(status).toMatchSnapshot();
});

/**
* Test utility to clone a fetch response for mocking purposes with the agent
* @param request - RequestInfo
* @param init - RequestInit
* @returns Promise<Response>
*/
export async function fetchCloner(
request: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
const response = await fetch(request, init);
const cloned = response.clone();
const responseBuffer = await cloned.arrayBuffer();

const mock = {
headers: [...response.headers.entries()],
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: toHex(responseBuffer),
now: Date.now(),
};

console.log(request);
console.log(JSON.stringify(mock));

return response;
}
25 changes: 14 additions & 11 deletions packages/agent/src/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { bufEquals, concat, fromHex, toHex } from './utils/buffer';
import { Principal } from '@dfinity/principal';
import * as bls from './utils/bls';
import { decodeTime } from './utils/leb';
import { MANAGEMENT_CANISTER_ID } from './agent';

/**
* A certificate may fail verification with respect to the provided public key
Expand Down Expand Up @@ -271,17 +272,19 @@ export class Certificate {

await cert.verify();

const canisterInRange = check_canister_ranges({
canisterId: this._canisterId,
subnetId: Principal.fromUint8Array(new Uint8Array(d.subnet_id)),
tree: cert.cert.tree,
});
if (!canisterInRange) {
throw new CertificateVerificationError(
`Canister ${this._canisterId} not in range of delegations for subnet 0x${toHex(
d.subnet_id,
)}`,
);
if (this._canisterId.toString() !== MANAGEMENT_CANISTER_ID) {
const canisterInRange = check_canister_ranges({
canisterId: this._canisterId,
subnetId: Principal.fromUint8Array(new Uint8Array(d.subnet_id)),
tree: cert.cert.tree,
});
if (!canisterInRange) {
throw new CertificateVerificationError(
`Canister ${this._canisterId} not in range of delegations for subnet 0x${toHex(
d.subnet_id,
)}`,
);
}
}
const publicKeyLookup = lookupResultToBuffer(
cert.lookup(['subnet', d.subnet_id, 'public_key']),
Expand Down
Loading