From 43800cdc6e613dc0ae95729a689d9a9e71f410cf Mon Sep 17 00:00:00 2001 From: Bruce Leidl Date: Sat, 2 Feb 2019 20:42:42 -0500 Subject: [PATCH] Initial implementation of keyring Keyring is an encrypted file to store secrets. The encryption key is derived from the disk decryption passphrase so that the file can be automatically decrypted and processed during boot. The keys contained in the keyring file are loaded into the kernel key store so that they can later be retrieved by other components. Currenly during installation a signing key is generated and stored in the keyring so that the system can transparently sign RealmFS images when the user modifies or updates them. --- citadel-tool/src/boot/mod.rs | 15 +- citadel-tool/src/install/installer.rs | 8 + libcitadel/src/keyring.rs | 253 ++++++++++++++++++++++++++ libcitadel/src/lib.rs | 2 + 4 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 libcitadel/src/keyring.rs 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";