diff --git a/citadel-tool/src/boot/mod.rs b/citadel-tool/src/boot/mod.rs index 046816e..2b0892c 100644 --- a/citadel-tool/src/boot/mod.rs +++ b/citadel-tool/src/boot/mod.rs @@ -1,7 +1,7 @@ use std::fs; use std::process::exit; -use libcitadel::{util,Result,ResourceImage,CommandLine,set_verbose,format_error}; +use libcitadel::{util,Result,ResourceImage,CommandLine,set_verbose,format_error,KeyRing}; mod live; mod disks; @@ -30,10 +30,21 @@ fn do_rootfs() -> Result<()> { if CommandLine::live_mode() || CommandLine::install_mode() { live::live_rootfs() } else { - rootfs::setup_rootfs() + rootfs::setup_rootfs()?; + if let Err(err) = setup_keyring() { + warn!("Failed to setup keyring: {}", err); + } + Ok(()) } } +fn setup_keyring() -> Result<()> { + ResourceImage::ensure_storage_mounted()?; + let keyring = KeyRing::load_with_cryptsetup_passphrase("/sysroot/storage/keyring")?; + keyring.add_keys_to_kernel()?; + Ok(()) +} + fn do_setup() -> Result<()> { if CommandLine::live_mode() || CommandLine::install_mode() { diff --git a/citadel-tool/src/install/installer.rs b/citadel-tool/src/install/installer.rs index 33ba089..f3e7dbc 100644 --- a/citadel-tool/src/install/installer.rs +++ b/citadel-tool/src/install/installer.rs @@ -10,6 +10,7 @@ use libcitadel::util::{self,mount,exec_cmdline_with_output}; use libcitadel::RealmFS; use libcitadel::Result; use libcitadel::OsRelease; +use libcitadel::KeyRing; const BLKDEACTIVATE: &str = "/sbin/blkdeactivate"; const CRYPTSETUP: &str = "/sbin/cryptsetup"; @@ -370,6 +371,7 @@ impl Installer { fn setup_storage(&self) -> Result<()> { if self._type == InstallType::Install { + self.create_keyring()?; self.setup_storage_resources()?; self.setup_base_realmfs()?; } @@ -383,6 +385,12 @@ impl Installer { Ok(()) } + fn create_keyring(&self) -> Result<()> { + self.header("Creating initial keyring")?; + let keyring = KeyRing::create_new(); + keyring.write(self.storage().join("keyring"), self.passphrase.as_ref().unwrap()) + } + fn setup_base_realmfs(&self) -> Result<()> { let realmfs_dir = self.storage().join("realms/realmfs-images"); fs::create_dir_all(&realmfs_dir)?; diff --git a/libcitadel/src/keyring.rs b/libcitadel/src/keyring.rs new file mode 100644 index 0000000..ba25d13 --- /dev/null +++ b/libcitadel/src/keyring.rs @@ -0,0 +1,253 @@ +use std::path::{Path,PathBuf}; +use std::collections::HashMap; +use std::io::{self,Read,Write}; +use std::fs; +use std::ffi::CString; +use std::os::raw::c_char; + +use libc::{self,c_long,c_ulong, c_int, int32_t}; + +use hex; +use sodiumoxide::randombytes::randombytes_into; +use sodiumoxide::crypto::{ + sign::{ + self, SEEDBYTES, + }, + pwhash::{ + self,SALTBYTES, Salt, + }, + secretbox::{ + self, NONCEBYTES, Nonce, + }, +}; + +use crate::{Result,Error}; + +#[derive(Serialize,Deserialize,Debug)] +pub struct KeyRing { + keypairs: HashMap, +} + +impl KeyRing { + pub fn create_new() -> KeyRing { + let seed = KeyRing::new_random_seed(); + let mut keypairs = HashMap::new(); + keypairs.insert("realmfs-user".to_string(), hex::encode(&seed.0)); + KeyRing { keypairs } + } + + pub fn load>(path: P, passphrase: &str) -> Result { + let mut sbox = SecretBox::new(path.as_ref()); + sbox.read()?; + let mut bytes = sbox.open(passphrase)?; + let keyring = toml::from_slice::(&bytes)?; + bytes.iter_mut().for_each(|b| *b = 0); + Ok(keyring) + } + + pub fn load_with_cryptsetup_passphrase>(path: P) -> Result { + let passphrase = KeyRing::get_cryptsetup_passphrase()?; + KeyRing::load(path, &passphrase) + } + + fn get_cryptsetup_passphrase() -> Result { + let key = KernelKey::request_key("user", "cryptsetup")?; + info!("Got key {}", key.0); + let buf = key.read()?; + match buf.split(|b| *b == 0).map(|bs| String::from_utf8_lossy(bs).to_string()).last() { + Some(s) => Ok(s), + None => Ok(String::new()), + } + } + + pub fn add_keys_to_kernel(&self) -> Result<()> { + for (k,v) in self.keypairs.iter() { + info!("Adding {} to kernel keystore", k.as_str()); + let _key = KernelKey::add_key("user", k.as_str(), v.as_bytes(), KEY_SPEC_USER_KEYRING)?; + } + Ok(()) + } + + pub fn write>(&self, path: P, passphrase: &str) -> Result<()> { + let salt = pwhash::gen_salt(); + let nonce = secretbox::gen_nonce(); + let key = SecretBox::passphrase_to_key(passphrase, &salt)?; + let bytes = toml::to_vec(self)?; + let ciphertext = secretbox::seal(&bytes, &nonce, &key); + + let mut file = fs::File::create(path.as_ref())?; + file.write_all(&salt.0)?; + file.write_all(&nonce.0)?; + file.write_all(&ciphertext)?; + Ok(()) + } + + fn new_random_seed() -> sign::Seed { + let mut seedbuf = [0; SEEDBYTES]; + randombytes_into(&mut seedbuf); + sign::Seed(seedbuf) + } + +} + +impl Drop for KeyRing { + fn drop(&mut self) { + for (_,v) in self.keypairs.drain() { + v.into_bytes().iter_mut().for_each(|b| *b = 0); + } + } +} + +struct SecretBox { + path: PathBuf, + salt: Salt, + nonce: Nonce, + data: Vec, +} + +impl SecretBox { + fn new(path: &Path) -> SecretBox { + SecretBox { + path: path.to_path_buf(), + salt: Salt([0; SALTBYTES]), + nonce: Nonce([0; NONCEBYTES]), + data: Vec::new(), + } + } + + fn read(&mut self) -> Result<()> { + if !self.data.is_empty() { + self.data.clear(); + } + let mut file = fs::File::open(&self.path)?; + file.read_exact(&mut self.salt.0)?; + file.read_exact(&mut self.nonce.0)?; + file.read_to_end(&mut self.data)?; + Ok(()) + } + + fn open(&self, passphrase: &str) -> Result> { + let key = SecretBox::passphrase_to_key(passphrase, &self.salt)?; + let result = secretbox::open(&self.data, &self.nonce, &key) + .map_err(|_| format_err!("Failed to decrypt {}", self.path.display()))?; + Ok(result) + } + + fn passphrase_to_key(passphrase: &str, salt: &Salt) -> Result { + let mut keybuf = [0; secretbox::KEYBYTES]; + pwhash::derive_key(&mut keybuf, passphrase.as_bytes(), salt, pwhash::OPSLIMIT_INTERACTIVE, pwhash::MEMLIMIT_INTERACTIVE) + .map_err(|_| format_err!("Failed to derive key"))?; + Ok(secretbox::Key(keybuf)) + } + + +} + +const KEYCTL_READ : c_int = 11; // read a key or keyring's contents +const KEY_SPEC_USER_KEYRING : c_int = -4; // - key ID for UID-specific keyring + + +pub struct KernelKey(int32_t); + +impl KernelKey { + pub fn request_key(key_type: &str, description: &str) -> Result { + let key_type = CString::new(key_type).unwrap(); + let description = CString::new(description).unwrap(); + let serial = _request_key(key_type.as_ptr(), description.as_ptr())?; + Ok(KernelKey(serial as i32)) + } + + pub fn add_key(key_type: &str, description: &str, payload: &[u8], ring_id: c_int) -> Result { + let key_type = CString::new(key_type).unwrap(); + let description = CString::new(description).unwrap(); + let serial = _add_key(key_type.as_ptr(), description.as_ptr(), payload.as_ptr(), payload.len(), ring_id)?; + Ok(KernelKey(serial as i32)) + } + + pub fn read(&self) -> Result> { + let mut size = 0; + loop { + size = match self.buffer_request(KEYCTL_READ, size) { + BufferResult::Err(err) => return Err(err), + BufferResult::Ok(buffer) => return Ok(buffer), + BufferResult::TooSmall(sz) => sz + 1, + } + } + } + + fn buffer_request(&self, command: c_int, size: usize) -> BufferResult { + if size == 0 { + return match keyctl1(command, self.id()) { + Err(err) => BufferResult::Err(err), + Ok(n) if n < 0 => BufferResult::Err(format_err!("keyctl returned bad size")), + Ok(n) => BufferResult::TooSmall(n as usize), + }; + } + let mut buffer = vec![0u8; size]; + match keyctl3(command, self.id(), buffer.as_ptr() as u64, buffer.len() as u64) { + Err(err) => BufferResult::Err(err), + Ok(n) if n < 0 => BufferResult::Err(format_err!("keyctrl returned bad size {}", n)), + Ok(sz) if size >= (sz as usize) => { + let sz = sz as usize; + if size > sz { + buffer.truncate(sz) + } + BufferResult::Ok(buffer) + }, + Ok(n) => BufferResult::TooSmall(n as usize) + } + } + + fn id(&self) -> c_ulong { + self.0 as c_ulong + } + +} + +enum BufferResult { + Ok(Vec), + Err(Error), + TooSmall(usize), +} + + +fn keyctl1(command: c_int, arg2: c_ulong) -> Result { + _keyctl(command, arg2, 0, 0, 0) +} + +fn keyctl3(command: c_int, arg2: c_ulong, arg3: c_ulong, arg4: c_ulong) -> Result { + _keyctl(command, arg2, arg3, arg4, 0) +} + +fn _keyctl(command: c_int, arg2: c_ulong, arg3: c_ulong, arg4: c_ulong, arg5: c_ulong) -> Result { + unsafe { + let r = libc::syscall(libc::SYS_keyctl, command, arg2, arg3, arg4, arg5); + if r == -1 { + Err(io::Error::last_os_error().into()) + } else { + Ok(r) + } + } +} + +fn _request_key(key_type: *const c_char, description: *const c_char) -> Result { + unsafe { + let r = libc::syscall(libc::SYS_request_key, key_type, description, 0, 0); + if r == -1 { + Err(io::Error::last_os_error().into()) + } else { + Ok(r) + } + } +} + +fn _add_key(key_type: *const c_char, description: *const c_char, payload: *const u8, plen: usize, ring_id: c_int) -> Result { + unsafe { + let r = libc::syscall(libc::SYS_add_key, key_type, description, payload, plen, ring_id); + if r == -1 { + Err(io::Error::last_os_error().into()) + } else { + Ok(r) + } + } +} \ No newline at end of file diff --git a/libcitadel/src/lib.rs b/libcitadel/src/lib.rs index 78a3959..ea6db22 100644 --- a/libcitadel/src/lib.rs +++ b/libcitadel/src/lib.rs @@ -59,6 +59,7 @@ pub mod util; pub mod verity; mod mount; mod realmfs; +mod keyring; pub use crate::config::OsRelease; pub use crate::blockdev::BlockDev; @@ -69,6 +70,7 @@ pub use crate::resource::ResourceImage; pub use crate::keys::{KeyPair,PublicKey}; pub use crate::mount::Mount; pub use crate::realmfs::RealmFS; +pub use crate::keyring::KeyRing; const DEVKEYS_HEX: &str = "bc02a3a4fd4a0471a8cb2f96d8be0a0a2d060798c024e60d7a98482f23197fc0";