Skip to content

Commit

Permalink
chore: skip range check when the certificate comes from the managemen…
Browse files Browse the repository at this point in the history
…t canister (#945)

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

* chore: throw UpdateCallRejectedError for v2 reject messages

* test: unit tests for canister_status reject and reply from management canister

* chore: changing condition for handling error responses

---------

Co-authored-by: Kai Peacock <[email protected]>
  • Loading branch information
dfx-json and krpeacock authored Oct 22, 2024
1 parent 69a92f7 commit b1b7540
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 12 deletions.
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.body && 'reject_message' in response.body) {
// 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

0 comments on commit b1b7540

Please sign in to comment.