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

feat: replica-signed queries #480

Merged
merged 44 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b5d7787
first draft of replica-signed queries
adamspofford-dfinity Sep 29, 2023
d30459f
Merge branch 'main' into spofford/rsq
adamspofford-dfinity Oct 3, 2023
67efb21
Fix signature verification
adamspofford-dfinity Oct 4, 2023
ff43070
Error if signature missing
adamspofford-dfinity Oct 4, 2023
d3213b4
minor future proofing
adamspofford-dfinity Oct 4, 2023
3f7a0d9
Accidental reversion
adamspofford-dfinity Oct 4, 2023
bc1ac8e
that warning doesn't work on wasm
adamspofford-dfinity Oct 4, 2023
f291d70
lost in translation
adamspofford-dfinity Oct 4, 2023
3fcdacd
Add the ability to disable query signature verification
adamspofford-dfinity Oct 4, 2023
c37f8f3
Fix ring dep
adamspofford-dfinity Oct 5, 2023
28c6d19
is chromedriver itself bugged?
adamspofford-dfinity Oct 5, 2023
47b015d
Yes, but it's also moka and ring's fault.
adamspofford-dfinity Oct 5, 2023
980479d
Verify the time on all certificates
adamspofford-dfinity Oct 5, 2023
b81d836
Enabled by default
adamspofford-dfinity Oct 5, 2023
8db73e0
Check that a canister is in the range
adamspofford-dfinity Oct 5, 2023
581452b
Account for baked-in state responses in agent_test
adamspofford-dfinity Oct 5, 2023
55a8842
not that it matters, but don't include the last byte of principals in…
adamspofford-dfinity Oct 10, 2023
c3d4752
.
adamspofford-dfinity Oct 10, 2023
2af59a1
Merge branch 'main' into spofford/rsq
adamspofford-dfinity Oct 17, 2023
8596a7f
Move timestamp check out of certificate function
adamspofford-dfinity Oct 20, 2023
55d7029
Add a couple of tests
adamspofford-dfinity Oct 20, 2023
224841c
Update response hasher so ref-tests pass
adamspofford-dfinity Oct 20, 2023
19d83d4
switch to ed25519-consensus, use specific error
adamspofford-dfinity Oct 24, 2023
8f4354a
fix wasm ref-test
adamspofford-dfinity Oct 24, 2023
b63ecf2
update dfx in ic-ref workflow
adamspofford-dfinity Oct 24, 2023
a0af723
Replace `pub use` with wrapper function for API compatibility
adamspofford-dfinity Oct 31, 2023
4fffb6b
Check signature timeout
adamspofford-dfinity Oct 31, 2023
b286e41
Fetch canister ranges from delegation instead of state tree
adamspofford-dfinity Oct 31, 2023
98891cb
Merge branch 'main' into spofford/rsq
adamspofford-dfinity Oct 31, 2023
56f57e6
Add warning comment
adamspofford-dfinity Oct 31, 2023
c830de8
Add the ability to specify a nonce
adamspofford-dfinity Oct 31, 2023
b1bfc29
Merge branch 'main' into spofford/rsq
adamspofford-dfinity Nov 2, 2023
4d52d21
Account for early failure for wrong subnet in test
adamspofford-dfinity Nov 3, 2023
a4f0fa8
👽
adamspofford-dfinity Nov 3, 2023
6294c67
Different error kind
adamspofford-dfinity Nov 3, 2023
37fcfc1
Merge branch 'main' into spofford/rsq
adamspofford-dfinity Nov 3, 2023
2c1c4e8
More error cases
adamspofford-dfinity Nov 6, 2023
872596d
dependabot
adamspofford-dfinity Nov 6, 2023
a6c4a5c
final protocol update
adamspofford-dfinity Nov 6, 2023
e6dc699
be precise about error variant
adamspofford-dfinity Nov 6, 2023
96fc7a7
Merge branch 'main' into spofford/rsq
adamspofford-dfinity Nov 6, 2023
3a5c3ec
round down instead of up, cap at 4 minutes instead of drift
adamspofford-dfinity Nov 7, 2023
643fbfc
Update tests
adamspofford-dfinity Nov 7, 2023
21c3a26
On second thought, don't hard-cap the duration
adamspofford-dfinity Nov 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-1

- name: Install wasm-pack
- name: Install wasm-pack and chromedriver
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
sudo apt-get install -y chromium-chromedriver

- name: Run Tests
shell: bash
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

* Added node signature certification to query calls, for protection against rogue boundary nodes. This can be disabled with `with_verify_query_signatures`.
* Added `with_nonce_generation` to `QueryBuilder` for precise cache control.
* Added `read_subnet_state_raw` to `Agent` and `read_subnet_state` to `Transport` for looking up raw state by subnet ID instead of canister ID.
* Added `read_state_subnet_metrics` to `Agent` to access subnet metrics, such as total spent cycles.
* Types passed to the `to_request_id` function can now contain nested structs, signed integers, and externally tagged enums.
Expand Down
77 changes: 77 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ic-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ include = ["src", "Cargo.toml", "../LICENSE", "README.md"]

[dependencies]
backoff = "0.4.0"
cached = { version = "0.46", features = ["ahash"], default-features = false }
candid = { workspace = true }
ed25519-consensus = { version = "2" }
futures-util = "0.3.21"
hex = { workspace = true }
http = "0.2.6"
Expand All @@ -29,6 +31,7 @@ leb128 = { workspace = true }
pkcs8 = { version = "0.10.2", features = ["std"] }
sec1 = { version = "0.7.2", features = ["pem"] }
rand = "0.8.5"
rangemap = "1.4"
ring = { workspace = true, features = ["std"] }
serde = { workspace = true, features = ["derive"] }
serde_bytes = { workspace = true }
Expand Down Expand Up @@ -102,6 +105,7 @@ wasm-bindgen = [
"dep:web-sys",
"time/wasm-bindgen",
"backoff/wasm-bindgen",
"cached/wasm",
]

[package.metadata.docs.rs]
Expand Down
39 changes: 28 additions & 11 deletions ic-agent/http_mock_service_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,47 @@ async function getMock(nonce) {
});
}

// Status codes are chosen to avoid being picked up as successes by tests expecting a 404 or 500.

self.addEventListener("fetch", (event) => {
event.respondWith((async () => {
try {
const request = event.request;
const url = new URL(request.url);
if (url.host === "mock_configure") {
const { method, path, status_code, nonce, body, headers } = await request.json();
await setMock({ method, path, status_code, nonce, body, headers, hits: 0 });
const nonce = url.pathname.substring(1);
const { method, path, status_code, body, headers } = await request.json();
const mock = await getMock(nonce) ?? { nonce, routes: [] };
mock.routes.push({ method, path, status_code, body, headers, hits: 0 });
await setMock(mock);
return new Response(null, { status: 204 });
} else if (url.host === "mock_assert") {
const nonce = url.pathname.substring(1);
const { hits } = await getMock(nonce);
return new Response(hits, { status: 200 });
const mock = await getMock(nonce);
if (mock === undefined) {
return new Response(`no such mock id ${nonce}`, { status: 421 });
}
const hitsMap = Object.fromEntries(mock.routes.map(route => [`${route.method} ${route.path}`, route.hits]));
return new Response(JSON.stringify(hitsMap), { status: 200, headers: { 'Content-Type': 'application/json' } });
} else {
const nonce = url.host.split('_')[1];
const { method, path, status_code, body, headers, hits } = await getMock(nonce);
if (request.method !== method) {
return new Response(`expected ${method}, got ${request.method}`, { status: 405 });
const mock = await getMock(nonce);
if (mock === undefined) {
return new Response(`no such mock id ${nonce}`, { status: 421 });
}
for (const route of mock.routes) {
if (request.method === route.method && url.pathname === route.path) {
route.hits += 1;
await setMock(mock);
return new Response(Uint8Array.from(route.body), { status: route.status_code, headers: route.headers });
}
}
if (url.pathname !== path) {
return new Response(`expcted ${path}, got ${url.pathname}`, { status: 404 });
const possiblyMeant = mock.routes.find(route => route.path === url.pathname);
if (possiblyMeant !== undefined) {
return new Response(`expected ${possiblyMeant.method}, got ${request.method}`, { status: 405 })
} else {
return new Response(`expected ${mock.routes.map(route => route.path).join(' | ')}, got ${url.pathname}`, { status: 410 });
}
await setMock({ method, path, status_code, nonce, body, headers, hits: hits + 1 });
return new Response(Uint8Array.from(body), { status: status_code, headers });
}
} catch (e) {
return new Response(e.toString(), { status: 503 });
Expand Down
5 changes: 4 additions & 1 deletion ic-agent/src/agent/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ pub struct AgentConfig {
pub identity: Arc<dyn Identity>,
/// See [`with_ingress_expiry`](super::AgentBuilder::with_ingress_expiry).
pub ingress_expiry: Option<Duration>,
/// The [`with_transport`](super::AgentBuilder::with_transport).
/// See [`with_transport`](super::AgentBuilder::with_transport).
pub transport: Option<Arc<dyn Transport>>,
/// See [`verify_query_signatures`](super::AgentBuilder::with_verify_query_signatures).
pub verify_query_signatures: bool,
}

impl Default for AgentConfig {
Expand All @@ -23,6 +25,7 @@ impl Default for AgentConfig {
identity: Arc::new(AnonymousIdentity {}),
ingress_expiry: None,
transport: None,
verify_query_signatures: true,
adamspofford-dfinity marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
15 changes: 14 additions & 1 deletion ic-agent/src/agent/agent_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{agent::status::Status, RequestIdError};
use ic_certification::Label;
use ic_transport_types::{InvalidRejectCodeError, RejectResponse};
use leb128::read;
use std::time::Duration;
use std::{
fmt::{Debug, Display, Formatter},
str::Utf8Error,
Expand Down Expand Up @@ -93,14 +94,26 @@ pub enum AgentError {
#[error("The request status ({1}) at path {0:?} is invalid.")]
InvalidRequestStatus(Vec<Label>, String),

/// The certificate verification failed.
/// The certificate verification for a read_state call failed.
#[error("Certificate verification failed.")]
CertificateVerificationFailed(),

/// The signature verification for a query call failed.
#[error("Query signature verification failed.")]
QuerySignatureVerificationFailed,

/// The certificate contained a delegation that does not include the effective_canister_id in the canister_ranges field.
#[error("Certificate is not authorized to respond to queries for this canister. While developing: Did you forget to set effective_canister_id?")]
CertificateNotAuthorized(),

/// The certificate was older than allowed by the `ingress_expiry`.
#[error("Certificate is stale (over {0:?}). Is the computer's clock synchronized?")]
CertificateOutdated(Duration),

/// The query response did not contain any node signatures.
#[error("Query response did not contain any node signatures")]
MissingSignature,

/// There was a length mismatch between the expected and actual length of the BLS DER-encoded public key.
#[error(
r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
Expand Down
Loading
Loading