diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9c86f7b..37992c1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,7 +24,7 @@ jobs: repo: cross matches: ${{ matrix.platform }} token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: cross-${{ matrix.platform }} path: ${{ steps.cross.outputs.install_path }} @@ -36,7 +36,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -44,14 +44,18 @@ jobs: override: true - name: Build run: cargo build --verbose + - name: Install request-key for Request/Instantiate flow + run: | + cargo build --example request-key --features std + sudo mv ./target/debug/examples/request-key /sbin/request-key - name: Run tests - run: cargo test --verbose + run: cargo test --verbose -- --include-ignored # Ensure clippy and formatting pass clippy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions-rs/toolchain@v1 with: profile: minimal @@ -71,14 +75,14 @@ jobs: runs-on: ubuntu-latest needs: install-cross steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: 'recursive' - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Download Cross - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: cross-linux-musl path: /tmp diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 643148d..7b1bc97 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install stable toolchain uses: actions-rs/toolchain@v1 @@ -23,14 +23,19 @@ jobs: toolchain: stable override: true + - name: Install cargo-tarpaulin + run: cargo install cargo-tarpaulin + + - name: Install request-key for Request/Instantiate flow + run: | + cargo build --example request-key --features std + sudo mv ./target/debug/examples/request-key /sbin/request-key + - name: Run cargo-tarpaulin - uses: actions-rs/tarpaulin@v0.1 - with: - version: '0.21.0' - args: "--lib" + run: cargo-tarpaulin --lib -- --include-ignored - name: Upload to codecov.io - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v5 with: file: ./cobertura.xml diff --git a/Cargo.toml b/Cargo.toml index 6b58bb7..082b4f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,14 @@ std = ["bitflags/std"] name = "keyctl" required-features = ["std"] +[[example]] +name = "request-key" +required-features = ["std"] + [dependencies] -libc = {version = "0.2.132", default-features = false} -bitflags = {version = "2.4", default-features = false} +libc = {version = "0.2.158", default-features = false} +bitflags = {version = "2.6", default-features = false} [dev-dependencies] -zeroize = "1.5.7" -clap = {version = "4.4.11", default-features = false, features = ["std", "derive"]} +zeroize = "1.8.1" +clap = {version = "4.5.16", default-features = false, features = ["std", "derive", "help"]} \ No newline at end of file diff --git a/examples/keyctl.rs b/examples/keyctl.rs index fbac329..171c3aa 100644 --- a/examples/keyctl.rs +++ b/examples/keyctl.rs @@ -3,19 +3,22 @@ //! //! Demo code for the linux_keyutils crate. use clap::Parser; +use linux_keyutils::{Key, KeyRing, KeyRingIdentifier, KeySerialId}; use linux_keyutils::{KeyPermissionsBuilder, Permission}; -use linux_keyutils::{KeyRing, KeyRingIdentifier}; use std::error::Error; use zeroize::Zeroizing; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] +#[command(arg_required_else_help(true))] +#[command(subcommand_required(true))] struct Args { #[clap(subcommand)] subcommand: Command, } #[derive(clap::Subcommand, Debug, PartialEq)] +#[command(arg_required_else_help(true))] enum Command { /// Create a new key Create { @@ -51,6 +54,17 @@ enum Command { #[clap(short, long)] description: String, }, + /// Instantiate a partially constructed key + Instantiate { + #[clap(short, long)] + keyid: Option, + + #[clap(short, long)] + payload: String, + + #[clap(short, long)] + ring: Option, + }, } fn main() -> Result<(), Box> { @@ -104,6 +118,15 @@ fn main() -> Result<(), Box> { key.invalidate()?; println!("Removed key with ID {:?}", key.get_id()); } + // Instantiate a partially constructed key + Command::Instantiate { + keyid, + payload, + ring, + } => { + let key = Key::from_id(KeySerialId::new(keyid.unwrap_or(i32::MAX))); + key.instantiate(&payload, KeySerialId::new(ring.unwrap_or(i32::MAX)))?; + } }; Ok(()) diff --git a/examples/request-key.rs b/examples/request-key.rs new file mode 100644 index 0000000..609d7ae --- /dev/null +++ b/examples/request-key.rs @@ -0,0 +1,65 @@ +//! Request Key Implementation (replacement for /sbin/request-key) +//! +//! https://www.kernel.org/doc/html/v4.15/security/keys/request-key.html +use clap::Parser; +use linux_keyutils::{Key, KeyRingIdentifier, KeySerialId}; +use std::error::Error; +use zeroize::Zeroizing; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +#[command(arg_required_else_help(true))] +#[command(subcommand_required(true))] +struct Args { + #[clap(subcommand)] + subcommand: Command, +} + +#[derive(clap::Subcommand, Debug, PartialEq)] +#[command(arg_required_else_help(true))] +enum Command { + /// Kernel invokes this program with the following parameters + /// + /// https://github.com/torvalds/linux/blob/7d06015d936c861160803e020f68f413b5c3cd9d/security/keys/request_key.c#L116 + /// + /// Path is hard coded to /sbin/request-key + Create { + key_id: i32, + uid: u32, + gid: u32, + thread_ring: i32, + process_ring: i32, + session_ring: i32, + }, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + _ = match args.subcommand { + // Add a new key to the keyring + Command::Create { + key_id, + uid, + gid, + thread_ring: _, + process_ring: _, + session_ring, + } => { + // Assume authority over the temporary key + let key = Key::from_id(KeySerialId(key_id)); + key.assume_authority()?; + + // Ensure the ownership is correct + key.chown(Some(uid), Some(gid))?; + + // Read payload from special key KeyRingIdentifier::ReqKeyAuthKey + let reqkey = Key::from_id(KeySerialId(KeyRingIdentifier::ReqKeyAuthKey as i32)); + let mut buf = Zeroizing::new([0u8; 2048]); + let len = reqkey.read(&mut buf)?; + + // Instantiate key + key.instantiate(&buf[..len], KeySerialId(session_ring))?; + } + }; + Ok(()) +} diff --git a/src/errors.rs b/src/errors.rs index 8d9e530..50c40b9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -54,6 +54,15 @@ pub enum KeyError { /// Write to destination failed WriteError, + // Insufficient permissions + PermissionDenied, + + // Missing file or directory (ENOENT) + // + // For request_key this could be due to a missing /sbin/request-key + // binary. I.e. keyutils utilities are not installed. + MissingFileOrDirectory, + /// Unknown - catch all, return this instead of panicing Unknown(i32), } @@ -73,6 +82,8 @@ impl KeyError { pub fn from_errno() -> KeyError { match unsafe { *libc::__errno_location() } { // Create Errors + libc::ENOENT => KeyError::MissingFileOrDirectory, + libc::EPERM => KeyError::PermissionDenied, libc::EACCES => KeyError::AccessDenied, libc::EDQUOT => KeyError::QuotaExceeded, libc::EFAULT => KeyError::BadAddress, diff --git a/src/ffi/functions.rs b/src/ffi/functions.rs index 91801d9..ae041ea 100644 --- a/src/ffi/functions.rs +++ b/src/ffi/functions.rs @@ -56,6 +56,65 @@ pub(crate) fn add_key( )) } +/// request_key() attempts to find a key of the given type with a description that +/// matches the specified description. If such a key could not be found, then +/// the key is optionally created. +/// +/// If the key is found or created, request_key() attaches it to the keyring +/// and returns the key's serial number. +/// +/// request_key() first recursively searches for a matching key in all of the keyrings +/// attached to the calling process. The keyrings are searched in the order: +/// thread-specific keyring, process-specific keyring, and then session keyring. +/// +/// If request_key() is called from a program invoked by request_key() on behalf +/// of some other process to generate a key, then the keyrings of that other process +/// will be searched next, using that other process's user ID, group ID, supplementary +/// group IDs, and security context to determine access. +/// +/// The search of the keyring tree is breadth-first: the keys in each keyring searched +/// are checked for a match before any child keyrings are recursed into. Only keys for +/// which the caller has search permission be found, and only keyrings for which the +/// caller has search permission may be searched. +/// +/// If the key is not found and callout info is empty then the call fails with the +/// error ENOKEY. +/// +/// If the key is not found and callout info is not empty, then the kernel attempts +/// to invoke a user-space program to instantiate the key. +pub(crate) fn request_key( + ktype: KeyType, + keyring: libc::c_ulong, + description: &str, + info: Option<&str>, +) -> Result { + // Perform conversion into a c string + let description = CString::new(description).or(Err(KeyError::InvalidDescription))?; + let callout = CString::new(info.unwrap_or("")).or(Err(KeyError::InvalidDescription))?; + + // Perform the actual system call. By setting callout to NULL the kernel will + // not invoke /sbin/request-key + let res = unsafe { + libc::syscall( + libc::SYS_request_key, + Into::<&'static CStr>::into(ktype).as_ptr(), + description.as_ptr(), + info.map_or_else(core::ptr::null, |_| callout.as_ptr()), + keyring as u32, + ) + }; + + // Return the underlying error + if res < 0 { + return Err(KeyError::from_errno()); + } + + // Otherwise return the ID + Ok(KeySerialId::new( + res.try_into().or(Err(KeyError::InvalidIdentifier))?, + )) +} + /// keyctl() allows user-space programs to perform key manipulation. /// /// The operation performed by keyctl() is determined by the value of the operation argument. diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index e80da1e..12caea7 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -21,7 +21,7 @@ macro_rules! keyctl { pub use types::*; #[allow(unused_imports)] -pub(crate) use functions::{add_key, keyctl_impl}; +pub(crate) use functions::{add_key, keyctl_impl, request_key}; // Export the macro for use pub(crate) use keyctl; diff --git a/src/ffi/types.rs b/src/ffi/types.rs index a4115e5..2a4e45f 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -147,13 +147,11 @@ impl KeySerialId { /// Using Rust's type system to ensure only valid strings are provided to the syscall. impl From for &'static CStr { fn from(t: KeyType) -> &'static CStr { - unsafe { - match t { - KeyType::KeyRing => CStr::from_bytes_with_nul_unchecked(b"keyring\0"), - KeyType::User => CStr::from_bytes_with_nul_unchecked(b"user\0"), - KeyType::Logon => CStr::from_bytes_with_nul_unchecked(b"logon\0"), - KeyType::BigKey => CStr::from_bytes_with_nul_unchecked(b"big_key\0"), - } + match t { + KeyType::KeyRing => c"keyring", + KeyType::User => c"user", + KeyType::Logon => c"logon", + KeyType::BigKey => c"big_key", } } } diff --git a/src/key.rs b/src/key.rs index 1dbeb25..d753702 100644 --- a/src/key.rs +++ b/src/key.rs @@ -120,7 +120,7 @@ impl Key { /// Change the permissions of the key with the ID provided /// /// If the caller doesn't have the CAP_SYS_ADMIN capability, it can change - /// permissions only only for the keys it owns. (More precisely: the caller's + /// permissions only for the keys it owns. (More precisely: the caller's /// filesystem UID must match the UID of the key.) pub fn set_perms(&self, perm: KeyPermissions) -> Result<(), KeyError> { _ = ffi::keyctl!( @@ -226,6 +226,49 @@ impl Key { )?; Ok(()) } + + /// Assume the authority for the calling thread to instantiate a key. + /// + /// Authority over a key can be assumed only if the calling thread has present + /// in its keyrings the authorization key that is associated with the specified key. + /// + /// In other words, the KEYCTL_ASSUME_AUTHORITY operation is available only from + /// a request-key(8)-style program. + /// + /// The caller must have search permission on the authorization key. + pub fn assume_authority(&self) -> Result<(), KeyError> { + ffi::keyctl!( + KeyCtlOperation::AssumeAuthority, + self.0.as_raw_id() as libc::c_ulong + )?; + Ok(()) + } + + /// Instantiate a partially constructed key. + /// + /// To instantiate a key, the caller must have the appropriate + /// authorization key. This is automatically granted when the caller + /// is invoked by /sbin/request-key. + pub fn instantiate + ?Sized>( + &self, + payload: &T, + id: KeySerialId, + ) -> Result<(), KeyError> { + // When instanting keyrings the payload will be NULL + let buffer = payload.as_ref(); + let (payload, plen) = match buffer.len() { + 0 => (core::ptr::null(), 0), + _ => (buffer.as_ptr(), buffer.len()), + }; + _ = ffi::keyctl!( + KeyCtlOperation::Instantiate, + self.0.as_raw_id() as libc::c_ulong, + payload as _, + plen as _, + id.as_raw_id() as libc::c_ulong + )?; + Ok(()) + } } #[cfg(test)] diff --git a/src/keyring.rs b/src/keyring.rs index c7951be..b1388a5 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -11,7 +11,7 @@ pub struct KeyRing { } impl KeyRing { - /// Initialize a new [Key] object from the provided ID + /// Initialize a new [KeyRing] object from the provided ID pub(crate) fn from_id(id: KeySerialId) -> Self { Self { id } } @@ -95,6 +95,33 @@ impl KeyRing { Ok(Key::from_id(id)) } + /// Attempts to find a key of the given type with a description that + /// matches the specified description. If such a key could not be found, + /// then the key is optionally created. + /// + /// If the key is found or created, it is attached it to the keyring + /// and returns the key's serial number. + /// + /// If the key is not found and callout info is empty then the call + /// fails with the error ENOKEY. + /// + /// If the key is not found and callout info is not empty, then the + /// kernel attempts to invoke a user-space program to instantiate the + /// key. + pub fn request_key + ?Sized, C: AsRef + ?Sized>( + &self, + description: &D, + callout: Option<&C>, + ) -> Result { + let id = ffi::request_key( + KeyType::User, + self.id.as_raw_id() as libc::c_ulong, + description.as_ref(), + callout.map(|c| c.as_ref()), + )?; + Ok(Key::from_id(id)) + } + /// Search for a key in the keyring tree, starting with this keyring as the head, /// returning its ID. /// @@ -269,6 +296,49 @@ mod test { // Assert that the ID is the same assert_eq!(key.get_id(), result.get_id()); + // Request should also succeed + let result = ring.request_key("test_search", None::<&str>).unwrap(); + + // Assert that the ID is the same + assert_eq!(key.get_id(), result.get_id()); + + // Invalidate the key + key.invalidate().unwrap(); + } + + #[test] + fn test_request_non_existing_key() { + // Test that a keyring that normally doesn't exist by default is + // created when called. + let ring = KeyRing::from_special_id(KeyRingIdentifier::Session, false).unwrap(); + + let result = ring.request_key("test_request_no_exist", None::<&str>); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), KeyError::KeyDoesNotExist); + } + + #[test] + #[ignore] + fn test_request_non_existing_key_callout() { + let callout = "Test Data from Callout"; + + // Test that a keyring that normally doesn't exist by default is + // created when called. + let ring = KeyRing::from_special_id(KeyRingIdentifier::Session, false).unwrap(); + + // The test expects that the key is instantiated by a program invoked by + // /sbin/request-key and that the key data is taken from the callout info + // passed here. + // + // The following examples/keyctl command in /etc/request-key.conf is known to work: + // create user test_callout * /path/to/examples/keyctl instantiate --keyid %k --payload %c --ring %S + let key = ring.request_key("test_callout", Some(callout)).unwrap(); + + // Verify the payload + let payload = key.read_to_vec().unwrap(); + assert_eq!(callout.as_bytes(), &payload); + // Invalidate the key key.invalidate().unwrap(); }