use anyhow::{ensure, Context, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use ed25519_dalek::{Signature, Signer as _, SigningKey, VerifyingKey};
use std::path::Path;
use zeroize::Zeroizing;
pub struct FileSigner(SigningKey);
impl FileSigner {
pub fn load_or_create(path: impl AsRef<Path>) -> Result<Self> {
let seed = load_or_create_secret(path.as_ref())?;
Ok(Self(SigningKey::from_bytes(&seed)))
}
}
impl crate::native::DeviceSigner for FileSigner {
fn public_jwk(&self, _app_tag: &str) -> Result<serde_json::Value> {
Ok(serde_json::json!({
"kty": "OKP", "crv": "Ed25519",
"x": URL_SAFE_NO_PAD.encode(self.0.verifying_key().as_bytes()),
}))
}
fn sign(&self, _app_tag: &str, challenge: &[u8]) -> Result<Vec<u8>> {
Ok(self.0.sign(challenge).to_bytes().to_vec())
}
}
impl crate::offline::OfflineSigner for FileSigner {
fn verifying_key(&self) -> Result<VerifyingKey> {
Ok(self.0.verifying_key())
}
fn sign(&self, message: &[u8]) -> Result<Signature> {
Ok(self.0.sign(message))
}
}
pub fn load_or_create_endpoint_key(path: impl AsRef<Path>) -> Result<iroh::SecretKey> {
let seed = load_or_create_secret(path.as_ref())?;
Ok(iroh::SecretKey::from_bytes(&seed))
}
#[cfg(not(unix))]
fn load_or_create_secret(_path: &Path) -> Result<Zeroizing<[u8; 32]>> {
anyhow::bail!(
"private file keys require Unix permissions; install a host-owned signer on this platform"
)
}
#[cfg(unix)]
fn load_or_create_secret(path: &Path) -> Result<Zeroizing<[u8; 32]>> {
use std::fs::{self, File, OpenOptions};
use std::io::{Read as _, Write as _};
use std::os::unix::fs::{
DirBuilderExt as _, MetadataExt as _, OpenOptionsExt as _, PermissionsExt as _,
};
ensure!(path.is_absolute(), "private key path must be absolute");
let parent = path
.parent()
.context("private key path requires a directory")?;
fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(parent)
.context("create private key directory")?;
let metadata = fs::symlink_metadata(parent).context("inspect private key directory")?;
ensure!(
metadata.is_dir()
&& !metadata.file_type().is_symlink()
&& metadata.permissions().mode() & 0o077 == 0
&& metadata.uid() == unsafe { libc::geteuid() },
"key directory must be private and owned by this user"
);
let read = || -> Result<Option<Zeroizing<[u8; 32]>>> {
let mut file = match OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
.open(path)
{
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error).context("open private key"),
};
let metadata = file.metadata()?;
ensure!(
metadata.is_file()
&& metadata.len() == 32
&& metadata.permissions().mode() & 0o077 == 0
&& metadata.uid() == unsafe { libc::geteuid() },
"private key file is invalid or not private"
);
let mut seed = Zeroizing::new([0u8; 32]);
file.read_exact(seed.as_mut()).context("read private key")?;
Ok(Some(seed))
};
if let Some(seed) = read()? {
return Ok(seed);
}
let mut seed = Zeroizing::new([0u8; 32]);
getrandom::getrandom(seed.as_mut())
.map_err(|error| anyhow::anyhow!("generate private key: {error}"))?;
let temp = parent.join(format!(".openrtc-key-{}.tmp", uuid::Uuid::new_v4()));
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&temp)
.context("create private key temporary file")?;
let result = (|| -> Result<()> {
file.write_all(seed.as_ref()).context("write private key")?;
file.sync_all().context("sync private key")?;
match fs::hard_link(&temp, path) {
Ok(()) => (),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => (),
Err(error) => return Err(error).context("install private key"),
}
Ok(())
})();
drop(file);
let cleanup = fs::remove_file(&temp);
result?;
cleanup.context("remove private key temporary file")?;
File::open(parent)?
.sync_all()
.context("sync private key directory")?;
read()?.context("private key disappeared during initialization")
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::offline::OfflineSigner;
use ed25519_dalek::Verifier as _;
use std::fs;
use std::os::unix::fs::{symlink, PermissionsExt as _};
struct Directory(std::path::PathBuf);
impl Directory {
fn new() -> Self {
Self(std::env::temp_dir().join(format!("openrtc-keys-{}", uuid::Uuid::new_v4())))
}
}
impl Drop for Directory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn reopen_preserves_separate_proof_and_endpoint_identities() {
let directory = Directory::new();
let proof = directory.0.join("proof.key");
let endpoint = directory.0.join("iroh.key");
let first = FileSigner::load_or_create(&proof).unwrap();
let public = first.verifying_key().unwrap();
let signature = first.sign(b"restart proof").unwrap();
drop(first);
let second = FileSigner::load_or_create(&proof).unwrap();
assert_eq!(second.verifying_key().unwrap(), public);
public.verify(b"restart proof", &signature).unwrap();
let endpoint_id = load_or_create_endpoint_key(&endpoint).unwrap().public();
assert_eq!(
load_or_create_endpoint_key(&endpoint).unwrap().public(),
endpoint_id
);
assert_ne!(endpoint_id.as_bytes(), public.as_bytes());
assert_eq!(
fs::metadata(&proof).unwrap().permissions().mode() & 0o077,
0
);
}
#[test]
fn simultaneous_creators_share_one_complete_key() {
let directory = Directory::new();
let key = directory.0.join("proof.key");
let barrier = std::sync::Arc::new(std::sync::Barrier::new(8));
let tasks: Vec<_> = (0..8)
.map(|_| {
let key = key.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
FileSigner::load_or_create(key)
.unwrap()
.verifying_key()
.unwrap()
})
})
.collect();
let keys: Vec<_> = tasks.into_iter().map(|task| task.join().unwrap()).collect();
assert!(keys.iter().all(|key| key == &keys[0]));
assert_eq!(fs::read_dir(&directory.0).unwrap().count(), 1);
}
#[test]
fn unsafe_or_damaged_paths_fail_without_rotating_identity() {
let directory = Directory::new();
let key = directory.0.join("proof.key");
FileSigner::load_or_create(&key).unwrap();
let original = fs::read(&key).unwrap();
let link = directory.0.join("link.key");
symlink(&key, &link).unwrap();
assert!(FileSigner::load_or_create(&link).is_err());
fs::set_permissions(&key, fs::Permissions::from_mode(0o644)).unwrap();
assert!(FileSigner::load_or_create(&key).is_err());
assert_eq!(fs::read(&key).unwrap(), original);
fs::set_permissions(&key, fs::Permissions::from_mode(0o600)).unwrap();
fs::write(&key, b"damaged").unwrap();
assert!(FileSigner::load_or_create(&key).is_err());
assert_eq!(fs::read(&key).unwrap(), b"damaged");
assert!(FileSigner::load_or_create("relative.key").is_err());
}
}