538 lines
19 KiB
Rust
538 lines
19 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
use crate::constants::{BRAIN_STAGING, BRAIN_TESTING, CONFIG_OVERRIDE_PATH_ENV};
|
|
use crate::general;
|
|
use crate::utils::block_on;
|
|
use ed25519_dalek::SigningKey;
|
|
use log::{debug, info, warn};
|
|
use openssl::bn::BigNum;
|
|
use openssl::hash::{Hasher, MessageDigest};
|
|
use openssl::pkey::{PKey, Private};
|
|
use openssl::rsa::Rsa;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
|
|
#[derive(Serialize, Default)]
|
|
pub struct AccountData {
|
|
path: String,
|
|
network: String,
|
|
ssh_pubkey: String,
|
|
account_balance: f64,
|
|
locked_funds: f64,
|
|
wallet_address: String,
|
|
wallet_path: String,
|
|
hratls_pubkey: String,
|
|
hratls_key_path: String,
|
|
mrsigner: String,
|
|
mrsigner_key_path: String,
|
|
}
|
|
|
|
impl super::HumanOutput for AccountData {
|
|
fn human_cli_print(&self) {
|
|
if !self.network.is_empty() {
|
|
println!("The network you are using is: {}", self.network);
|
|
}
|
|
if !self.ssh_pubkey.is_empty() {
|
|
println!("The SSH key used for VMs is: {}", self.ssh_pubkey);
|
|
}
|
|
if !self.wallet_path.is_empty() {
|
|
println!("The address of your DeTEE wallet is {}", self.wallet_address);
|
|
println!("The balance of your account is {} credits", self.account_balance);
|
|
if self.locked_funds != 0.0 {
|
|
println!(
|
|
"WARNING! {} credits is temporary locked, waiting for a Contract.",
|
|
self.locked_funds
|
|
);
|
|
}
|
|
if self.account_balance == 0.0 {
|
|
println!("Hop on discord to get an airdrop: https://discord.gg/DcfYczAMtD \n")
|
|
}
|
|
}
|
|
if !self.mrsigner.is_empty() {
|
|
println!("The MRSIGNER for apps is: {}", self.mrsigner);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
|
pub struct Config {
|
|
ssh_key_path: String,
|
|
#[serde(default = "default_network")]
|
|
network: String,
|
|
}
|
|
|
|
fn default_network() -> String {
|
|
// default to testnet
|
|
// TODO: remove instruction from docs to set brain_url, since it defaults now
|
|
"testnet".to_string()
|
|
}
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum Error {
|
|
#[error("Failed to get access to disk: {0}")]
|
|
DiskAccess(#[from] std::io::Error),
|
|
#[error("Parsing of the yaml config file failed: {0}")]
|
|
YamlFormat(#[from] serde_yaml::Error),
|
|
#[error("The private key of the DeTEE account got corrupted.")]
|
|
CorruptedWalletKey,
|
|
#[error("The private ED25519 key used for HRATLS got corrupted: {0}")]
|
|
CorruptedHratlsKey(String),
|
|
#[error("The MRSIGNER key (used for signing apps) got corrupted: {0}")]
|
|
CorruptedMrSigner(String),
|
|
#[error("Failed to generate key using openssl: {0}")]
|
|
Openssl(String),
|
|
#[error{"Failed to retrive/download the artefact"}]
|
|
ArtefactError,
|
|
#[error{"SSH key not defined. Run `detee-cli account` for more info."}]
|
|
SshKeyNoDefined,
|
|
#[error{"RSA Error: {0}"}]
|
|
RSAError(#[from] openssl::error::ErrorStack),
|
|
#[error{"Internal CLI error: {0}"}]
|
|
InternalError(String),
|
|
#[error(transparent)]
|
|
BrainConnection(#[from] tonic::transport::Error),
|
|
}
|
|
|
|
impl Config {
|
|
fn save_to_disk(&self) -> Result<(), Error> {
|
|
let path = Self::config_path()?;
|
|
debug!("Writing config file to disk at {}", path);
|
|
let mut file = File::create(path)?;
|
|
file.write_all(serde_yaml::to_string(self)?.as_bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn home_dir() -> String {
|
|
match std::env::var("HOME") {
|
|
Ok(path) => path,
|
|
Err(_) => {
|
|
// TODO: considering lowering log priority from warn to debug.
|
|
warn!(
|
|
"Could not find HOME env variable. Config will get saved to /opt/.detee/cli/"
|
|
);
|
|
"/opt".to_string()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn logs_dir() -> Result<String, Error> {
|
|
let dir = Self::home_dir() + ("/.detee/cli/logs");
|
|
if !Path::new(&dir).exists() {
|
|
warn!("Could not config dir. Creating {dir}");
|
|
std::fs::create_dir_all(dir.clone())?;
|
|
}
|
|
Ok(dir)
|
|
}
|
|
|
|
pub fn verify_and_install_artefacts(file_hash: &str) -> Result<(), Error> {
|
|
use std::fs;
|
|
let artefacts_dir = Config::artefacts_dir()? + "/";
|
|
let file_path = Path::new(&artefacts_dir).join(file_hash);
|
|
|
|
if file_path.exists() {
|
|
log::debug!("Artefact '{}' already exists", file_hash);
|
|
return Ok(());
|
|
}
|
|
log::debug!("Artefact '{}' not found. Proceeding to download...", file_hash);
|
|
let url = format!("https://registry.detee.ltd/{}", file_hash);
|
|
|
|
let response = match reqwest::blocking::get(&url) {
|
|
Ok(resp) => resp,
|
|
Err(err) => {
|
|
log::error!("Failed to download artifact: {}", err);
|
|
return Err(Error::ArtefactError);
|
|
}
|
|
};
|
|
|
|
if !response.status().is_success() {
|
|
log::error!("Failed to download artifact: server responded with {}", response.status());
|
|
return Err(Error::ArtefactError);
|
|
}
|
|
|
|
let content = match response.bytes() {
|
|
Ok(bytes) => bytes,
|
|
Err(err) => {
|
|
log::error!("Error reading artifact data from response: {}", err);
|
|
return Err(Error::ArtefactError);
|
|
}
|
|
};
|
|
|
|
if let Err(e) = fs::write(&file_path, &content) {
|
|
log::error!("Error writing artifact to file: {}", e);
|
|
return Err(Error::ArtefactError);
|
|
}
|
|
|
|
log::debug!("Downloaded atrifact {}", file_hash);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn artefacts_dir() -> Result<String, Error> {
|
|
let dir = Self::home_dir() + "/.detee/artefacts";
|
|
if !Path::new(&dir).exists() {
|
|
info!("Creating {dir}");
|
|
warn!("Since {dir} does not exist, also assume meaurement will fail cause you need the artefacts.");
|
|
std::fs::create_dir_all(dir.clone())?;
|
|
}
|
|
Ok(dir)
|
|
}
|
|
|
|
pub fn vm_id_list_path() -> Result<String, Error> {
|
|
let dir = Self::home_dir() + ("/.detee/cli/vms");
|
|
if !Path::new(&dir).exists() {
|
|
std::fs::create_dir_all(dir.clone())?;
|
|
}
|
|
Ok(dir + "/vm_id_list")
|
|
}
|
|
|
|
pub fn cli_dir_path() -> Result<String, Error> {
|
|
let dir = std::env::var(CONFIG_OVERRIDE_PATH_ENV)
|
|
.unwrap_or_else(|_| Self::home_dir() + ("/.detee/cli"));
|
|
|
|
if !Path::new(&dir).exists() {
|
|
warn!("Could not config dir. Creating {dir}");
|
|
std::fs::create_dir_all(dir.clone())?;
|
|
}
|
|
Ok(dir)
|
|
}
|
|
|
|
fn config_path() -> Result<String, Error> {
|
|
let config_path = Self::cli_dir_path()? + ("/cli-config.yaml");
|
|
Ok(config_path)
|
|
}
|
|
|
|
fn detee_wallet_key_path() -> Result<String, Error> {
|
|
let config_path = Self::cli_dir_path()? + ("/secret_detee_wallet_key");
|
|
Ok(config_path)
|
|
}
|
|
|
|
fn load_config_from_file() -> Result<Self, Error> {
|
|
Ok(serde_yaml::from_str(&std::fs::read_to_string(Self::config_path()?)?)?)
|
|
}
|
|
|
|
pub fn init_config() -> Self {
|
|
// TODO: create if it does not exist
|
|
let config = match Self::load_config_from_file() {
|
|
Ok(config) => config,
|
|
Err(e) => {
|
|
log::error!("Could not load config due to error: {e}");
|
|
eprintln!("Config file not found. Creating new config file!");
|
|
let config = Self::default();
|
|
if let Err(e) = config.save_to_disk() {
|
|
log::error!("Could not save config to disk: {e}");
|
|
panic!("Could not initialize config.");
|
|
};
|
|
config
|
|
}
|
|
};
|
|
config
|
|
}
|
|
|
|
fn create_wallet_key() -> Result<(), Error> {
|
|
use rand::rngs::OsRng;
|
|
let mut csprng = OsRng;
|
|
|
|
let key_path = Self::detee_wallet_key_path()?;
|
|
if Path::new(&key_path).exists() {
|
|
log::debug!("Found SNP admin key at {key_path}");
|
|
return Ok(());
|
|
}
|
|
let account_key = SigningKey::generate(&mut csprng);
|
|
|
|
let mut account_file = File::create(key_path)?;
|
|
account_file.write_all(bs58::encode(account_key.to_bytes()).into_string().as_bytes())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn load_wallet_key() -> Result<SigningKey, Error> {
|
|
Self::create_wallet_key()?;
|
|
Ok(SigningKey::from_bytes(
|
|
&bs58::decode(std::fs::read_to_string(Self::detee_wallet_key_path()?)?.trim())
|
|
.into_vec()
|
|
.map_err(|_| Error::CorruptedWalletKey)?
|
|
.try_into()
|
|
.map_err(|_| Error::CorruptedWalletKey)?,
|
|
))
|
|
}
|
|
|
|
pub fn get_detee_wallet() -> Result<String, Error> {
|
|
Ok(bs58::encode(Self::load_wallet_key()?.verifying_key().to_bytes()).into_string())
|
|
}
|
|
|
|
pub fn try_sign_message(message: &str) -> Result<String, Error> {
|
|
use ed25519_dalek::Signer;
|
|
let key = Self::load_wallet_key()?;
|
|
Ok(bs58::encode(key.sign(message.as_bytes()).to_bytes()).into_string())
|
|
}
|
|
|
|
fn try_sign_file(&self, path: &str) -> Result<String, Error> {
|
|
use ed25519_dalek::Signer;
|
|
let file_bytes = std::fs::read(path)?;
|
|
let key = Self::load_wallet_key()?;
|
|
Ok(bs58::encode(key.sign(&file_bytes).to_bytes()).into_string())
|
|
}
|
|
|
|
pub fn sign_file(&self, path: &str) {
|
|
match self.try_sign_file(path) {
|
|
Ok(s) => println!("{s}"),
|
|
Err(e) => {
|
|
log::error!("coult not sign file due to error: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_ssh_pubkey_path(ssh_pubkey_path: &str) {
|
|
let mut config = Self::init_config();
|
|
match std::fs::read_to_string(ssh_pubkey_path) {
|
|
Ok(content) => {
|
|
info!("Found the file {ssh_pubkey_path}");
|
|
info!("The file contains the following data:\n{content}");
|
|
info!("This will be used as your public key.");
|
|
}
|
|
Err(e) => {
|
|
log::error!("Opening {ssh_pubkey_path} returned an error: {e:?}");
|
|
warn!("Cancelling operation!");
|
|
return;
|
|
}
|
|
};
|
|
config.ssh_key_path = ssh_pubkey_path.to_string();
|
|
if let Err(e) = config.save_to_disk() {
|
|
log::error!("Could not save new SSH key path to config: {e}");
|
|
}
|
|
}
|
|
|
|
pub fn get_ssh_pubkey(&self) -> Result<String, Error> {
|
|
if self.ssh_key_path.is_empty() {
|
|
return Err(Error::SshKeyNoDefined);
|
|
}
|
|
Ok(self.ssh_key_path.clone())
|
|
}
|
|
|
|
pub fn get_root_ca_path() -> Result<String, Error> {
|
|
Ok(Self::home_dir() + "/.detee/certs/root_ca.pem")
|
|
}
|
|
|
|
pub fn get_brain_info() -> (String, String) {
|
|
match Self::init_config().network.as_str() {
|
|
"localhost" => ("https://localhost:31337".to_string(), "staging-brain".to_string()),
|
|
"staging" => {
|
|
let url = BRAIN_STAGING.to_string();
|
|
log::debug!("Using staging brain URL: {url}");
|
|
(url, "staging-brain".to_string())
|
|
}
|
|
_ => {
|
|
let url = BRAIN_TESTING.to_string();
|
|
log::debug!("Using testnet brain URL: {url}");
|
|
(url, "testnet-brain".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn connect_brain_channel(
|
|
brain_url: String,
|
|
) -> Result<tonic::transport::Channel, Error> {
|
|
use hyper_rustls::HttpsConnectorBuilder;
|
|
use rustls::pki_types::pem::PemObject;
|
|
use rustls::pki_types::CertificateDer;
|
|
use rustls::{ClientConfig, RootCertStore};
|
|
|
|
let brain_san = Config::get_brain_info().1;
|
|
|
|
let mut detee_root_ca_store = RootCertStore::empty();
|
|
detee_root_ca_store
|
|
.add(CertificateDer::from_pem_file(Config::get_root_ca_path()?).map_err(|e| {
|
|
Error::InternalError(format!("Could not parse PEM certificate: {e}"))
|
|
})?)
|
|
.unwrap();
|
|
|
|
let client_tls_config = ClientConfig::builder()
|
|
.with_root_certificates(detee_root_ca_store)
|
|
.with_no_client_auth();
|
|
let connector = HttpsConnectorBuilder::new()
|
|
.with_tls_config(client_tls_config)
|
|
.https_only()
|
|
.with_server_name_resolver(hyper_rustls::FixedServerNameResolver::new(
|
|
brain_san.clone().try_into().map_err(|e| {
|
|
Error::InternalError(format!(
|
|
"Could not parse {brain_san} into domain resolver: {e}"
|
|
))
|
|
})?,
|
|
))
|
|
.enable_http2()
|
|
.build();
|
|
Ok(tonic::transport::Channel::from_shared(brain_url.to_string())
|
|
.map_err(|e| {
|
|
Error::InternalError(format!(
|
|
"Could not parse {brain_san} into domain resolver: {e}"
|
|
))
|
|
})?
|
|
.connect_with_connector(connector)
|
|
.await?)
|
|
}
|
|
|
|
pub fn set_network(mut network: &str) {
|
|
if network != "staging" && network != "testnet" && network != "localhost" {
|
|
log::error!(
|
|
"The network {network} is not officially supported. Defaulting to testnet!"
|
|
);
|
|
network = "testnet";
|
|
}
|
|
let mut config = Self::init_config();
|
|
info!("Setting the network to {network}");
|
|
config.network = network.to_string();
|
|
if let Err(e) = config.save_to_disk() {
|
|
log::error!("Could not save new brain URL to config: {e}");
|
|
}
|
|
}
|
|
|
|
pub fn get_account_data() -> AccountData {
|
|
let mut account_data = AccountData { ..Default::default() };
|
|
let config = Self::init_config();
|
|
match Self::config_path() {
|
|
Ok(path) => account_data.path = path,
|
|
Err(e) => {
|
|
println!("Could not read config due to error: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
if config.network.is_empty() {
|
|
log::error!("The network is not set! To configure it, run:");
|
|
eprintln!(" detee-cli account network testnet");
|
|
} else {
|
|
account_data.network = config.network;
|
|
}
|
|
if config.ssh_key_path.is_empty() {
|
|
log::error!("SSH public key path not set! To configure it, run:");
|
|
eprintln!(" detee-cli account ssh-pubkey-path /home/your_user/.ssh/id_ed25519.pub");
|
|
} else {
|
|
account_data.ssh_pubkey = config.ssh_key_path;
|
|
}
|
|
match Self::get_detee_wallet() {
|
|
Ok(key) => {
|
|
account_data.wallet_address = key.to_string();
|
|
match block_on(general::grpc::get_balance(&key)) {
|
|
Ok(account) => {
|
|
account_data.account_balance = account.balance as f64 / 1_000_000_000.0;
|
|
account_data.locked_funds = account.tmp_locked as f64 / 1_000_000_000.0;
|
|
}
|
|
Err(e) => log::error!("Could not get account balance due to error: {e}"),
|
|
}
|
|
}
|
|
Err(e) => log::error!("Please report this bug. Could not load admin key: {e}"),
|
|
}
|
|
match Self::detee_wallet_key_path() {
|
|
Ok(path) => account_data.wallet_path = path,
|
|
Err(_) => log::error!("This error should never happen. Please report this bug."),
|
|
}
|
|
|
|
match Self::mrsigner_key_path() {
|
|
Ok(path) => {
|
|
account_data.mrsigner_key_path = path;
|
|
match Self::get_mrsigner() {
|
|
Ok(mrsigner) => account_data.mrsigner = mrsigner,
|
|
Err(e) => {
|
|
log::error!("Could not load MRSIGNER key: {e}")
|
|
}
|
|
}
|
|
}
|
|
Err(e) => log::error!("Please report this bug. Could not get MRSIGNER path: {e}"),
|
|
}
|
|
|
|
match Self::hratls_key_path() {
|
|
Ok(path) => {
|
|
account_data.hratls_key_path = path;
|
|
match Self::get_hratls_pubkey_hex() {
|
|
Ok(pubkey) => account_data.hratls_pubkey = pubkey,
|
|
Err(e) => {
|
|
log::error!("Could not load HRATLS key: {e}")
|
|
}
|
|
}
|
|
}
|
|
Err(e) => log::error!("Please report this bug. Could not get HRATLS key path: {e}"),
|
|
}
|
|
|
|
account_data
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
pub fn get_hratls_private_key() -> Result<String, Error> {
|
|
let private_key_path = Self::hratls_key_path()?;
|
|
if Path::new(&private_key_path).exists() {
|
|
log::debug!("Found HRaTLS private key at {private_key_path}");
|
|
return std::fs::read_to_string(private_key_path)
|
|
.map_err(|e| Error::CorruptedHratlsKey(e.to_string()));
|
|
}
|
|
let key = PKey::generate_ed25519()?;
|
|
let mut key_file = File::create(private_key_path)?;
|
|
let pem_pkcs8 = key.private_key_to_pem_pkcs8()?;
|
|
key_file.write_all(&pem_pkcs8)?;
|
|
String::from_utf8(pem_pkcs8).map_err(|e| Error::CorruptedHratlsKey(e.to_string()))
|
|
}
|
|
|
|
pub fn get_hratls_pubkey_hex() -> Result<String, Error> {
|
|
let private_key_pem_str = Self::get_hratls_private_key()?;
|
|
let private_key = PKey::private_key_from_pem(private_key_pem_str.as_ref())?;
|
|
let pubkey = private_key.raw_public_key()?;
|
|
Ok(pubkey.iter().fold(String::new(), |acc, x| acc + &format!("{:02X?}", x)))
|
|
}
|
|
|
|
pub fn hratls_key_path() -> Result<String, Error> {
|
|
Ok(Self::cli_dir_path()? + ("/hratls_private_key.pem"))
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
pub fn get_mrsigner() -> Result<String, Error> {
|
|
let mut signing_key_mod = Self::get_mrsigner_rsa_key()?.n().to_vec();
|
|
signing_key_mod.reverse(); // make it little endian
|
|
|
|
// TODO: double check if hasher can actually fail
|
|
let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap();
|
|
hasher.update(&signing_key_mod).unwrap();
|
|
let mr_signer_raw = hasher.finish().unwrap();
|
|
|
|
let mut mr_signer = [0u8; 32];
|
|
mr_signer.copy_from_slice(&mr_signer_raw[..32]);
|
|
|
|
Ok(mr_signer.iter().fold(String::new(), |acc, x| acc + &format!("{:02X?}", x)))
|
|
}
|
|
|
|
fn get_mrsigner_rsa_key() -> Result<Rsa<Private>, Error> {
|
|
let signing_key_pem_str = Self::create_mrsigner_rsa_key()?;
|
|
Rsa::private_key_from_pem(signing_key_pem_str.as_ref())
|
|
.map_err(|e| Error::CorruptedMrSigner(e.to_string()))
|
|
}
|
|
|
|
fn create_mrsigner_rsa_key() -> Result<String, Error> {
|
|
let signing_key_path = Self::mrsigner_key_path()?;
|
|
if Path::new(&signing_key_path).exists() {
|
|
log::debug!("Found signing_key at {signing_key_path}");
|
|
return std::fs::read_to_string(signing_key_path)
|
|
.map_err(|e| Error::CorruptedMrSigner(e.to_string()));
|
|
}
|
|
let key = Rsa::generate_with_e(3072, BigNum::from_u32(3)?.as_ref())?;
|
|
let mut key_file = File::create(signing_key_path)?;
|
|
let pem_pkcs8 = key.private_key_to_pem()?;
|
|
key_file.write_all(&pem_pkcs8)?;
|
|
String::from_utf8(pem_pkcs8).map_err(|e| Error::CorruptedMrSigner(e.to_string()))
|
|
}
|
|
|
|
pub fn mrsigner_key_path() -> Result<String, Error> {
|
|
Ok(Self::cli_dir_path()? + ("/app_signing_key.pem"))
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
pub fn app_id_list_path() -> Result<String, Error> {
|
|
let dir = Self::home_dir() + ("/.detee/cli/apps");
|
|
if !Path::new(&dir).exists() {
|
|
std::fs::create_dir_all(dir.clone())?;
|
|
}
|
|
Ok(dir + "/app_id_list")
|
|
}
|
|
}
|