diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cd32877a795..2d50cb37c82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,6 +32,10 @@ jobs: toolchain: 1.71 - name: Build run: cargo build --workspace --all-targets --verbose ${{ matrix.cargo_flags }} + - name: Start Signing Agents + run: | + source <(gpg-agent --daemon) + eval `ssh-agent -s` - name: Test run: cargo test --workspace --all-targets --verbose ${{ matrix.cargo_flags }} env: diff --git a/flake.nix b/flake.nix index 01ca4aab45d..d0ddf59729f 100644 --- a/flake.nix +++ b/flake.nix @@ -82,7 +82,10 @@ installShellFiles makeWrapper pkg-config - gnupg # for signing tests + + # for signing tests + gnupg + openssh ] ++ linuxNativeDeps; buildInputs = with pkgs; [ openssl zstd libgit2 libssh2 @@ -147,6 +150,7 @@ # To run the signing tests gnupg + openssh # For building the documentation website poetry diff --git a/lib/tests/runner.rs b/lib/tests/runner.rs index a6f140ec4e4..44f939ef3d2 100644 --- a/lib/tests/runner.rs +++ b/lib/tests/runner.rs @@ -14,6 +14,7 @@ mod test_default_revset_graph_iterator; mod test_diff_summary; mod test_git; mod test_git_backend; +mod test_gpg; mod test_id_prefix; mod test_index; mod test_init; @@ -29,6 +30,6 @@ mod test_refs; mod test_revset; mod test_rewrite; mod test_signing; +mod test_ssh_signing; mod test_view; mod test_workspace; -mod test_gpg; diff --git a/lib/tests/test_ssh_signing.rs b/lib/tests/test_ssh_signing.rs new file mode 100644 index 00000000000..6cb80f09549 --- /dev/null +++ b/lib/tests/test_ssh_signing.rs @@ -0,0 +1,164 @@ +use std::io::Write; +use std::process::Stdio; + +use jj_lib::signing::{SigStatus, SigningBackend}; +use jj_lib::ssh_signing::SshBackend; + +static PRIVATE_KEY: &str = r#"-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBo/iejekjvuD/HTman0daImstssYYR52oB+dmr1KsOYQAAAIiuGFMFrhhT +BQAAAAtzc2gtZWQyNTUxOQAAACBo/iejekjvuD/HTman0daImstssYYR52oB+dmr1KsOYQ +AAAECcUtn/J/jk/+D5+/+WbQRNN4eInj5L60pt6FioP0nQfGj+J6N6SO+4P8dOZqfR1oia +y2yxhhHnagH52avUqw5hAAAAAAECAwQF +-----END OPENSSH PRIVATE KEY----- +"#; + +static PUBLIC_KEY: &str = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGj+J6N6SO+4P8dOZqfR1oiay2yxhhHnagH52avUqw5h"; + +struct SshEnvironment { + private_key: Option, + allowed_signers: Option, +} + +impl SshEnvironment { + fn new() -> Result { + let mut private_key = tempfile::Builder::new() + .prefix("jj-test-pk-") + .tempfile() + .unwrap(); + + private_key.write_all(PRIVATE_KEY.as_bytes()).unwrap(); + private_key.flush().unwrap(); + + let private_key_path = private_key.into_temp_path(); + + let ssh_add = std::process::Command::new("ssh-add") + .arg(private_key_path.as_os_str()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + + let res = ssh_add.wait_with_output().unwrap(); + if !res.status.success() { + println!("Failed to add private key to ssh-agent. Make sure it is running!"); + println!("{}", String::from_utf8_lossy(&res.stderr)); + return Err(res); + } + + let mut env = SshEnvironment { + private_key: Some(private_key_path), + allowed_signers: None, + }; + + env.with_good_public_key(); + + Ok(env) + } + + fn with_good_public_key(&mut self) { + let mut allowed_signers = tempfile::Builder::new() + .prefix("jj-test-allowed-signers-") + .tempfile() + .unwrap(); + + allowed_signers + .write_all("test@example.com ".as_bytes()) + .unwrap(); + allowed_signers.write_all(PUBLIC_KEY.as_bytes()).unwrap(); + allowed_signers.flush().unwrap(); + + let allowed_signers_path = allowed_signers.into_temp_path(); + + self.allowed_signers = Some(allowed_signers_path); + } + + fn with_bad_public_key(&mut self) { + let mut allowed_signers = tempfile::Builder::new() + .prefix("jj-test-allowed-signers-") + .tempfile() + .unwrap(); + + allowed_signers + .write_all("test@example.com ".as_bytes()) + .unwrap(); + allowed_signers + .write_all("INVALID PUBLIC KEY".as_bytes()) + .unwrap(); + allowed_signers.flush().unwrap(); + + let allowed_signers_path = allowed_signers.into_temp_path(); + + self.allowed_signers = Some(allowed_signers_path); + } +} + +impl Drop for SshEnvironment { + fn drop(&mut self) { + let mut ssh_add = std::process::Command::new("ssh-add") + .arg("-d") + .arg("-") + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + + ssh_add + .stdin + .as_mut() + .unwrap() + .write_all(PUBLIC_KEY.as_bytes()) + .unwrap(); + + ssh_add.stdin.as_mut().unwrap().flush().unwrap(); + + ssh_add.wait().unwrap(); + drop(self.private_key.take()); + drop(self.allowed_signers.take()); + } +} + +fn backend(env: &SshEnvironment) -> SshBackend { + SshBackend::new( + "ssh-keygen".into(), + Some(env.allowed_signers.as_ref().unwrap().as_os_str().into()), + ) +} + +#[test] +fn roundtrip() { + let env = SshEnvironment::new().unwrap(); + let backend = backend(&env); + let data = b"hello world"; + + let signature = backend.sign(data, Some(PUBLIC_KEY)).unwrap(); + + let check = backend.verify(data, &signature).unwrap(); + assert_eq!(check.status, SigStatus::Good); + assert_eq!(check.backend(), None); // backend is set by the signer + + assert_eq!(check.display.unwrap(), "test@example.com"); + + let check = backend.verify(b"invalid-commit-data", &signature).unwrap(); + assert_eq!(check.status, SigStatus::Bad); + assert_eq!(check.backend(), None); + assert_eq!(check.display.unwrap(), "test@example.com"); +} + +#[test] +fn bad_allowed_signers() { + let mut env = SshEnvironment::new().unwrap(); + env.with_bad_public_key(); + + let backend = backend(&env); + let data = b"hello world"; + + let signature = backend.sign(data, Some(PUBLIC_KEY)).unwrap(); + + let check = backend.verify(data, &signature).unwrap(); + assert_eq!(check.status, SigStatus::Unknown); + assert_eq!(check.display.unwrap(), "Signature OK. Unknown principal"); +}