diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 940dbf1a6a..b27f44eca0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,24 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + # The default version of gpg installed on the runners is a version baked in with git + # which only contains the components needed by git and doesn't work for our test cases. + # + # This installs the latest gpg4win version, which is a variation of GnuPG built for + # Windows. + # + # There is some issue with windows PATH max length which is what all the PATH wrangling + # below is for. Please see the below link for where this fix was derived from: + # https://github.com/orgs/community/discussions/24933 + - name: Setup GnuPG [windows] + if: ${{ matrix.os == 'windows-latest' }} + run: | + $env:PATH = "C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\chocolatey\bin" + [Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine") + choco install --yes gpg4win + echo "C:\Program Files (x86)\Gpg4win\..\GnuPG\bin" >> $env:GITHUB_PATH + - name: Install Rust uses: dtolnay/rust-toolchain@1482605bfc5719782e1267fd0c0cc350fe7646b8 with: diff --git a/flake.nix b/flake.nix index 2a964ebbc4..08cc7e452f 100644 --- a/flake.nix +++ b/flake.nix @@ -62,7 +62,7 @@ in { packages = { - jujutsu = ourRustPlatform.buildRustPackage rec { + jujutsu = ourRustPlatform.buildRustPackage { pname = "jujutsu"; version = "unstable-${self.shortRev or "dirty"}"; @@ -82,6 +82,7 @@ installShellFiles makeWrapper pkg-config + gnupg # for signing tests ] ++ linuxNativeDeps; buildInputs = with pkgs; [ openssl zstd libgit2 libssh2 @@ -144,6 +145,9 @@ # In case you need to run `cargo run --bin gen-protos` protobuf + # To run the signing tests + gnupg + # For building the documentation website poetry ] ++ darwinDeps ++ linuxNativeDeps; diff --git a/lib/tests/runner.rs b/lib/tests/runner.rs index e9d76cde48..554ea485e8 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; diff --git a/lib/tests/test_gpg.rs b/lib/tests/test_gpg.rs new file mode 100644 index 0000000000..028fc37c4e --- /dev/null +++ b/lib/tests/test_gpg.rs @@ -0,0 +1,186 @@ +#[cfg(unix)] +use std::fs::Permissions; +use std::io::Write; +#[cfg(unix)] +use std::os::unix::prelude::PermissionsExt; +use std::process::Stdio; + +use assert_matches::assert_matches; +use insta::assert_debug_snapshot; +use jj_lib::gpg_signing::GpgBackend; +use jj_lib::signing::{SigStatus, SignError, SigningBackend}; + +static PRIVATE_KEY: &str = r#"-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEZWI3pBYJKwYBBAHaRw8BAQdAaPLTNADvDWapjAPlxaUnx3HXQNIlwSz4EZrW +3Z7hxSwAAP9liwHZWJCGI2xW+XNqMT36qpIvoRcd5YPaKYwvnlkG1w+UtDNTb21l +b25lIChqaiB0ZXN0IHNpZ25pbmcga2V5KSA8c29tZW9uZUBleGFtcGxlLmNvbT6I +kwQTFgoAOxYhBKWOXukGcVPI9eXp6WOHhcsW/qBhBQJlYjekAhsDBQsJCAcCAiIC +BhUKCQgLAgQWAgMBAh4HAheAAAoJEGOHhcsW/qBhyBgBAMph1HkBkKlrZmsun+3i +kTEaOsWmaW/D6NEdMFiw0S/jAP9G3jOYGiZbUN3dWWB2246Oi7SaMTX8Xb2BrLP2 +axCbC5RYBGVjxv8WCSsGAQQB2kcPAQEHQE8Oa4ahtVG29gIRssPxjqF4utn8iHPz +m5z/8lX/nl3eAAD5AZ6H2pNhiy2gnGkbPLHw3ZyY4d0NXzCa7qc9EXqOj+sRrLQ9 +U29tZW9uZSBFbHNlIChqaiB0ZXN0IHNpZ25pbmcga2V5KSA8c29tZW9uZS1lbHNl +QGV4YW1wbGUuY29tPoiTBBMWCgA7FiEER1BAaEpU3TKUiUvFTtVW6XKeAA8FAmVj +xv8CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQTtVW6XKeAA/6TQEA +2DkPm3LmH8uG6qLirtf62kbG7T+qljIsarQKFw3CGakA/AveCtrL7wVSpINiu1Rz +lBqJFFP2PqzT0CRfh94HSIMM +=6JC8 +-----END PGP PRIVATE KEY BLOCK----- +"#; + +struct GpgEnvironment { + homedir: tempfile::TempDir, +} + +impl GpgEnvironment { + fn new() -> Result { + let dir = tempfile::Builder::new() + .prefix("jj-gpg-signing-test-") + .tempdir() + .unwrap(); + + let path = dir.path(); + + #[cfg(unix)] + std::fs::set_permissions(path, Permissions::from_mode(0o700)).unwrap(); + + let mut gpg = std::process::Command::new("gpg") + .arg("--homedir") + .arg(path) + .arg("--import") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + + gpg.stdin + .as_mut() + .unwrap() + .write_all(PRIVATE_KEY.as_bytes()) + .unwrap(); + + gpg.stdin.as_mut().unwrap().flush().unwrap(); + + let res = gpg.wait_with_output().unwrap(); + + if !res.status.success() { + println!("Failed to add private key to gpg-agent. Make sure it is running!"); + println!("{}", String::from_utf8_lossy(&res.stderr)); + return Err(res); + } + + Ok(GpgEnvironment { homedir: dir }) + } +} + +fn backend(env: &GpgEnvironment) -> GpgBackend { + // don't really need faked time for current tests, + // but probably will need it for end-to-end cli tests + GpgBackend::new("gpg".into(), false).with_extra_args(&[ + "--homedir".into(), + env.homedir.path().as_os_str().into(), + "--faked-system-time=1701042000!".into(), + ]) +} + +#[test] +fn gpg_singing_roundtrip() { + let env = GpgEnvironment::new().unwrap(); + let backend = backend(&env); + let data = b"hello world"; + let signature = backend.sign(data, None).unwrap(); + + let check = backend.verify(data, &signature).unwrap(); + assert_eq!(check.status, SigStatus::Good); + assert_eq!(check.key.unwrap(), "638785CB16FEA061"); + assert_eq!( + check.display.unwrap(), + "Someone (jj test signing key) " + ); + + let check = backend.verify(b"so so bad", &signature).unwrap(); + assert_eq!(check.status, SigStatus::Bad); + assert_eq!(check.key.unwrap(), "638785CB16FEA061"); + assert_eq!( + check.display.unwrap(), + "Someone (jj test signing key) " + ); +} + +#[test] +fn gpg_signing_roundtrip_explicit_key() { + let env = GpgEnvironment::new().unwrap(); + let backend = backend(&env); + let data = b"hello world"; + let signature = backend.sign(data, Some("Someone Else")).unwrap(); + + assert_debug_snapshot!(backend.verify(data, &signature).unwrap(), @r###" + Verification { + status: Good, + key: Some( + "4ED556E9729E000F", + ), + display: Some( + "Someone Else (jj test signing key) ", + ), + } + "###); + assert_debug_snapshot!(backend.verify(b"so so bad", &signature).unwrap(), @r###" + Verification { + status: Bad, + key: Some( + "4ED556E9729E000F", + ), + display: Some( + "Someone Else (jj test signing key) ", + ), + } + "###); +} + +#[test] +fn unknown_key() { + let env = GpgEnvironment::new().unwrap(); + let backend = backend(&env); + let signature = br"-----BEGIN PGP SIGNATURE----- + + iHUEABYKAB0WIQQs238pU7eC/ROoPJ0HH+PjJN1zMwUCZWPa5AAKCRAHH+PjJN1z + MyylAP9WQ3sZdbC4b1C+/nxs+Wl+rfwzeQWGbdcsBMyDABcpmgD/U+4KdO7eZj/I + e+U6bvqw3pOBoI53Th35drQ0qPI+jAE= + =kwsk + -----END PGP SIGNATURE-----"; + assert_debug_snapshot!(backend.verify(b"hello world", signature).unwrap(), @r###" + Verification { + status: Unknown, + key: Some( + "071FE3E324DD7333", + ), + display: None, + } + "###); + assert_debug_snapshot!(backend.verify(b"so bad", signature).unwrap(), @r###" + Verification { + status: Unknown, + key: Some( + "071FE3E324DD7333", + ), + display: None, + } + "###); +} + +#[test] +fn invalid_signature() { + let env = GpgEnvironment::new().unwrap(); + let backend = backend(&env); + let signature = br"-----BEGIN PGP SIGNATURE----- + + super duper invalid + -----END PGP SIGNATURE-----"; + assert_matches!( + backend.verify(b"hello world", signature), + Err(SignError::InvalidSignatureFormat) + ); +}