From d4deccefd38250629bdf406f6ebcdc54a4abc8c5 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Wed, 7 Feb 2024 14:09:25 +0000 Subject: [PATCH] Add ssh signing backend tests --- .github/workflows/build.yml | 25 ++++++ flake.nix | 11 ++- lib/tests/runner.rs | 2 + lib/tests/test_ssh_signing.rs | 164 ++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 lib/tests/test_ssh_signing.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4e08036dcc3..6cb27ad40e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,31 @@ jobs: choco install --yes gpg4win echo "C:\Program Files (x86)\Gpg4win\..\GnuPG\bin" >> $env:GITHUB_PATH + # The default version of openssh on windows server is quite old (8.1) and doesn't have + # all the necessary signing/verification commands available (such as -Y find-principals) + - name: Setup ssh-agent [windows] + if: ${{ matrix.os == 'windows-latest' }} + run: | + Remove-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 + Remove-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 + choco install openssh --pre + + $sshAgentOutput = ssh-agent -s | Out-String + + if ($sshAgentOutput -match 'SSH_AUTH_SOCK=(.+?); export SSH_AUTH_SOCK;') { + "SSH_AUTH_SOCK=$($matches[1])" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 + } + if ($sshAgentOutput -match 'SSH_AGENT_PID=([0-9]+); export SSH_AGENT_PID;') { + "SSH_AGENT_PID=$($matches[1])" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 + } + + - name: Setup ssh-agent [unix] + if: ${{ matrix.os != 'windows-latest' }} + run: | + eval "$(ssh-agent -s)" + echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> "$GITHUB_ENV" + echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> "$GITHUB_ENV" + - name: Install Rust uses: dtolnay/rust-toolchain@1482605bfc5719782e1267fd0c0cc350fe7646b8 with: diff --git a/flake.nix b/flake.nix index bad3526dc75..3183951f9c9 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 @@ -94,7 +97,10 @@ NIX_JJ_GIT_HASH = self.rev or ""; CARGO_INCREMENTAL = "0"; - preCheck = "export RUST_BACKTRACE=1"; + preCheck = '' + export RUST_BACKTRACE=1 + eval "$(ssh-agent -s)" + ''; postInstall = '' $out/bin/jj util mangen > ./jj.1 installManPage ./jj.1 @@ -147,6 +153,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..c3219166f6c 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,7 @@ 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"); +}