detee-cli/src/config.rs

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")
}
}