Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions ic-agent/src/agent/agent_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ pub enum AgentError {

#[error("The request status ({1}) at path {0:?} is invalid.")]
InvalidRequestStatus(Vec<Label>, String),

#[error("Certificate verification failed.")]
CertificateVerificationFailed(),

#[error(
r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."#
)]
DerKeyLengthMismatch { expected: usize, actual: usize },

#[error("BLS DER-encoded public key is invalid. Expected the following prefix: ${expected:?}, but got ${actual:?}")]
DerPrefixMismatch { expected: Vec<u8>, actual: Vec<u8> },

#[error("The status response did not contain a root key")]
NoRootKeyInStatus(),

#[error("Failed to initialize the BLS library")]
BlsInitializationFailure(),
}

impl PartialEq for AgentError {
Expand Down
112 changes: 108 additions & 4 deletions ic-agent/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub use response::{Replied, RequestStatusResponse};
mod agent_test;

use crate::agent::replica_api::{
AsyncContent, Certificate, Envelope, ReadStateResponse, SyncContent,
AsyncContent, Certificate, Delegation, Envelope, ReadStateResponse, SyncContent,
};
use crate::export::Principal;
use crate::hash_tree::{Label, LookupResult};
Expand All @@ -28,11 +28,19 @@ use reqwest::Method;
use serde::Serialize;
use status::Status;

use crate::bls::bls12381::bls;
use std::convert::TryFrom;
use std::str::from_utf8;
use std::sync::Once;
use std::sync::RwLock;
use std::time::Duration;

const DOMAIN_SEPARATOR: &[u8; 11] = b"\x0Aic-request";
const IC_REQUEST_DOMAIN_SEPARATOR: &[u8; 11] = b"\x0Aic-request";
const IC_STATE_ROOT_DOMAIN_SEPARATOR: &[u8; 14] = b"\x0Dic-state-root";
const DER_PREFIX: &[u8; 37] = b"\x30\x81\x82\x30\x1d\x06\x0d\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x01\x02\x01\x06\x0c\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x02\x01\x03\x61\x00";
const KEY_LENGTH: usize = 96;

static INIT_BLS: Once = Once::new();

/// A low level Agent to make calls to a Replica endpoint.
///
Expand Down Expand Up @@ -67,6 +75,7 @@ const DOMAIN_SEPARATOR: &[u8; 11] = b"\x0Aic-request";
/// .with_url(URL)
/// .with_identity(create_identity())
/// .build()?;
/// agent.fetch_root_key().await?;
/// let management_canister_id = Principal::from_text("aaaaa-aa")?;
///
/// let waiter = delay::Delay::builder()
Expand Down Expand Up @@ -101,6 +110,7 @@ pub struct Agent {
identity: Box<dyn Identity + Send + Sync>,
password_manager: Option<Box<dyn PasswordManager + Send + Sync>>,
ingress_expiry_duration: Duration,
root_key: RwLock<Vec<u8>>,
}

impl Agent {
Expand All @@ -112,6 +122,8 @@ impl Agent {

/// Create an instance of an [`Agent`].
pub fn new(config: AgentConfig) -> Result<Agent, AgentError> {
initialize_bls()?;

let url = config.url;
let mut tls_config = rustls::ClientConfig::new();

Expand All @@ -136,9 +148,25 @@ impl Agent {
ingress_expiry_duration: config
.ingress_expiry_duration
.unwrap_or_else(|| Duration::from_secs(300)),
root_key: RwLock::new(vec![]),
})
}

/// Fetch the root key from the status endpoint.
/// It is not necessary to call this when communicating with "the" Internet Computer.
pub async fn fetch_root_key(&self) -> Result<(), AgentError> {
let status = self.status().await?;
let root_key = status.root_key.ok_or_else(AgentError::NoRootKeyInStatus)?;
let mut write_guard = self.root_key.write().unwrap();
*write_guard = root_key;
Ok(())
}

fn read_root_key(&self) -> Result<Vec<u8>, AgentError> {
let root_key = self.root_key.read().unwrap().clone();
Ok(root_key)
}

fn get_expiry_date(&self) -> u64 {
// TODO(hansl): evaluate if we need this on the agent side (my hunch is we don't).
let permitted_drift = Duration::from_secs(60);
Expand All @@ -152,7 +180,7 @@ impl Agent {

fn construct_message(&self, request_id: &RequestId) -> Vec<u8> {
let mut buf = vec![];
buf.extend_from_slice(DOMAIN_SEPARATOR);
buf.extend_from_slice(IC_REQUEST_DOMAIN_SEPARATOR);
buf.extend_from_slice(request_id.as_slice());
buf
}
Expand Down Expand Up @@ -384,10 +412,51 @@ impl Agent {

let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate)
.map_err(AgentError::InvalidCborData)?;
// todo: verify certificate here
self.verify(&cert)?;
Ok(cert)
}

fn verify(&self, cert: &Certificate) -> Result<(), AgentError> {
let sig = &cert.signature;

let root_hash = cert.tree.digest();
let mut msg = vec![];
msg.extend_from_slice(IC_STATE_ROOT_DOMAIN_SEPARATOR);
msg.extend_from_slice(&root_hash);

let der_key = self.check_delegation(&cert.delegation)?;
let key = extract_der(der_key)?;

let result = bls::core_verify(sig, &*msg, &*key);
if result != bls::BLS_OK {
Err(AgentError::CertificateVerificationFailed())
} else {
Ok(())
}
}

fn check_delegation(&self, delegation: &Option<Delegation>) -> Result<Vec<u8>, AgentError> {
match delegation {
None => self.read_root_key(),
Some(delegation) => {
let cert: Certificate = serde_cbor::from_slice(&delegation.certificate)
.map_err(AgentError::InvalidCborData)?;
self.verify(&cert)?;
let public_key_path = vec![
"subnet".into(),
delegation.subnet_id.clone().into(),
"public_key".into(),
];
match cert.tree.lookup_path(&public_key_path) {
LookupResult::Absent => Err(AgentError::LookupPathAbsent(public_key_path)),
LookupResult::Unknown => Err(AgentError::LookupPathAbsent(public_key_path)),
LookupResult::Found(root_key) => Ok(root_key.to_vec()),
LookupResult::Error => Err(AgentError::LookupPathError(public_key_path)),
}
}
}
}

pub async fn request_status_raw(
&self,
request_id: &RequestId,
Expand Down Expand Up @@ -428,6 +497,41 @@ impl Agent {
}
}

fn initialize_bls() -> Result<(), AgentError> {
let mut init_err = None;
INIT_BLS.call_once(|| {
if bls::init() != bls::BLS_OK {
init_err = Some(AgentError::BlsInitializationFailure());
}
});
if let Some(init_err) = init_err {
Err(init_err)
} else {
Ok(())
}
}

fn extract_der(buf: Vec<u8>) -> Result<Vec<u8>, AgentError> {
let expected_length = DER_PREFIX.len() + KEY_LENGTH;
if buf.len() != expected_length {
return Err(AgentError::DerKeyLengthMismatch {
expected: expected_length,
actual: buf.len(),
});
}

let prefix = &buf[0..DER_PREFIX.len()];
if prefix[..] != DER_PREFIX[..] {
return Err(AgentError::DerPrefixMismatch {
expected: DER_PREFIX.to_vec(),
actual: prefix.to_vec(),
});
}

let key = &buf[DER_PREFIX.len()..];
Ok(key.to_vec())
}

fn lookup_request_status(
certificate: Certificate,
request_id: &RequestId,
Expand Down
11 changes: 11 additions & 0 deletions ic-agent/src/agent/replica_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ pub(crate) struct Certificate {

#[serde(with = "serde_bytes")]
pub signature: Vec<u8>,

pub delegation: Option<Delegation>,
}

#[derive(Deserialize)]
pub(crate) struct Delegation {
#[serde(with = "serde_bytes")]
pub subnet_id: Vec<u8>,

#[serde(with = "serde_bytes")]
pub certificate: Vec<u8>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
Expand Down
11 changes: 11 additions & 0 deletions ic-agent/src/agent/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub struct Status {
/// Optional. The precise git revision of the Internet Computer implementation.
pub impl_revision: Option<String>,

/// Optional. The root key
pub root_key: Option<Vec<u8>>,

/// Contains any additional values that the replica gave as status.
pub values: BTreeMap<String, Box<Value>>,
}
Expand Down Expand Up @@ -136,12 +139,20 @@ impl std::convert::TryFrom<&serde_cbor::Value> for Status {
None
}
});
let root_key: Option<Vec<u8>> = map.get("root_key").and_then(|v| {
if let Value::Bytes(bytes) = v.as_ref() {
Some(bytes.to_owned())
} else {
None
}
});

Ok(Status {
ic_api_version,
impl_source,
impl_version,
impl_revision,
root_key,
values: map,
})
}
Expand Down
1 change: 1 addition & 0 deletions ic-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
//! .with_url(URL)
//! .with_identity(create_identity())
//! .build()?;
//! agent.fetch_root_key().await?;
//! let management_canister_id = Principal::from_text("aaaaa-aa")?;
//!
//! let waiter = delay::Delay::builder()
Expand Down
4 changes: 4 additions & 0 deletions ref-tests/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ where
let agent = create_agent(agent_identity)
.await
.expect("Could not create an agent.");
agent
.fetch_root_key()
.await
.expect("could not fetch root key");
match f(agent).await {
Ok(_) => {}
Err(e) => assert!(false, "{:?}", e),
Expand Down
2 changes: 2 additions & 0 deletions ref-tests/tests/ic-ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ mod management_canister {
let other_agent_identity = create_identity().await?;
let other_agent_principal = other_agent_identity.sender()?;
let other_agent = create_agent(other_agent_identity).await?;
other_agent.fetch_root_key().await?;
let other_ic00 = ManagementCanister::create(&other_agent);

// Reinstall with another agent should fail.
Expand Down Expand Up @@ -382,6 +383,7 @@ mod management_canister {
// Create another agent with different identity.
let other_agent_identity = create_identity().await?;
let other_agent = create_agent(other_agent_identity).await?;
other_agent.fetch_root_key().await?;
let other_ic00 = ManagementCanister::create(&other_agent);

// Start as a wrong controller should fail.
Expand Down