Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 118 additions & 6 deletions crates/provenance/examples/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,71 @@ use fips204::{
ml_dsa_65,
traits::{KeyGen, SerDes},
};
use panll_provenance::*;
use panll_provenance::{attest, hash, sign, Envelope, PublicKey, Record, Registry, ALGORITHM};
use std::{fs, io::Write, path::Path};

fn private_directory(
directory: &Path,
configured: &[(&str, std::path::PathBuf)],
) -> Result<(), String> {
let directory = fs::canonicalize(directory).map_err(|e| e.to_string())?;
if directory
.ancestors()
.any(|ancestor| ancestor.join(".git").exists())
{
return Err("store the private key outside a Git working tree".into());
}
// Git also supports markerless worktrees through external metadata.
// Filesystem checks alone cannot establish whether this is tracked storage.
let mut command = std::process::Command::new("git");
command
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(directory)
.env("LC_ALL", "C");
for name in ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"] {
command.env_remove(name);
}
for (name, value) in configured {
command.env(name, value);
}
let output = command
.output()
.map_err(|_| "Git is required to validate the private-key destination")?;
if output.status.success() {
return match String::from_utf8_lossy(&output.stdout).trim() {
"true" => Err("store the private key outside a Git working tree".into()),
"false" => Ok(()),
_ => Err("Git returned an indeterminate working-tree result".into()),
};
}
if configured.is_empty()
&& String::from_utf8_lossy(&output.stderr).starts_with("fatal: not a git repository")
{
return Ok(());
}
Err("Git could not validate the private-key destination".into())
}

fn git_directories() -> Result<Vec<(&'static str, std::path::PathBuf)>, String> {
let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
Ok(["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR"]
.into_iter()
.filter_map(|name| {
std::env::var_os(name).map(|path| {
let path = std::path::PathBuf::from(path);
(
name,
if path.is_absolute() {
path
} else {
cwd.join(path)
},
)
})
})
.collect())
}

fn read<T: serde::de::DeserializeOwned>(path: &str) -> Result<T, String> {
serde_json::from_slice(&fs::read(path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())
}
Expand Down Expand Up @@ -43,10 +105,7 @@ fn run(args: &[String]) -> Result<(), String> {
[command, identity, from, until, public, private] if command == "keygen" => {
// Refuse a private key inside any Git working tree.
let directory = Path::new(private).parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
if std::process::Command::new("git").args(["rev-parse", "--is-inside-work-tree"])
.current_dir(directory).output().map_err(|e| e.to_string())?.status.success() {
return Err("store the private key outside a Git working tree".into());
}
private_directory(directory, &git_directories()?)?;
let (pk, sk) = ml_dsa_65::KG::try_keygen().map_err(str::to_owned)?;
let bytes = pk.into_bytes();
let key = PublicKey { key_id: hash(&bytes), author_identity: identity.clone(), algorithm: ALGORITHM.into(),
Expand All @@ -58,7 +117,7 @@ fn run(args: &[String]) -> Result<(), String> {
[command, record, private, output] if command == "sign" => {
let record: Record = read(record)?;
let signature = Some(sign(&record, &secret(private)?)?);
let envelope = Envelope { record, signature, attestations: vec![] };
let envelope = Envelope { record, signature: signature.into(), attestations: vec![] };
write_new(output, &serde_json::to_vec_pretty(&envelope).map_err(|e| e.to_string())?, false)
},
[command, input, public, private, output] if command == "witness" => {
Expand All @@ -78,3 +137,56 @@ fn main() {
std::process::exit(1);
}
}

#[cfg(test)]
mod tests {
use super::private_directory;
use std::{fs, process::Command};

#[test]
fn rejects_external_git_metadata_and_accepts_an_untracked_private_directory() {
let root = std::env::temp_dir().join(format!(
"panll-keyguard-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let work = root.join("work");
let metadata = root.join("metadata");
let outside = root.join("outside");
fs::create_dir_all(&work).unwrap();
fs::create_dir_all(&outside).unwrap();
assert!(Command::new("git")
.args(["init", "--bare", "--quiet"])
.arg(&metadata)
.status()
.unwrap()
.success());
assert!(Command::new("git")
.arg("--git-dir")
.arg(&metadata)
.args(["config", "core.bare", "false"])
.status()
.unwrap()
.success());
assert!(Command::new("git")
.arg("--git-dir")
.arg(&metadata)
.args(["config", "core.worktree"])
.arg(&work)
.status()
.unwrap()
.success());
assert!(!work.join(".git").exists());
assert!(private_directory(&work, &[("GIT_DIR", metadata.clone())]).is_err());
assert!(private_directory(
&work,
&[("GIT_DIR", metadata), ("GIT_WORK_TREE", work.clone())]
)
.is_err());
assert!(private_directory(&outside, &[]).is_ok());
fs::remove_dir_all(root).unwrap();
}
}
Loading
Loading