-
Notifications
You must be signed in to change notification settings - Fork 356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Display commit signatures #4853
base: main
Are you sure you want to change the base?
Conversation
760fda2
to
b000597
Compare
d1a1a72
to
4e6ba53
Compare
Is there anything blocking this PR? It seems to be in a functional state. |
Well, since I simply materialized the changes from @julienvincent and @necauqua, there are still two things missing: Regarding printing hints about dropped signatures, I reached out for help (#4712 (comment), #4712 (comment)). Maybe we could merge without the hints (and create a new issue to track that missing piece to be implemented later)? Then I would focus on getting the docs ready. |
Based on the PR title, I would presume that this PR is focused mainly on being able to display commit signatures. So it seems fine to me if printing hints about dropped commit signatures is shifted to a different issue/PR, since it isn't directly related to displaying commit signatures. |
Sounds good to me. |
4e6ba53
to
f4d1de5
Compare
I pushed a slightly modified version of 739ddf2. However, I believe it was planned to add a
@necauqua, originally this was your work, I think. Was it planned to add |
b59be44
to
8a8e8b5
Compare
@julienvincent, I added you as co-author to the commits. Would you like to provide the email address you used to sign the CLA, so I can add you as a co-author? |
Thanks for attribution - I just went ahead and signed the CLA for the email address you used. I reran the check which is now passing 👍🏻 |
Yeah, there might be a better way to organize the verification API, but I have no idea. @necauqua ? I think that's good as a start, btw. |
With gpg you have to verify to get any info, not sure about ssh, but probably same The only thing you (kind of obviously) get for free is checking if there's a signature at all With that in mind maybe it should be something like:
I vaguely recall that in the code the verification result is the thing that contains key/display/status, so we just reflect that structure |
3d6f4a6
to
8a379f0
Compare
Thanks for your feedback, @yuja and @necauqua. I implemented it as suggested, with the exception of using If you disagree, I am happy to change it back to |
Isn't I'll take a look maybe tomorrow, thanks. |
The point is that if there is a signature then the Technically we could probably make the Option => Boolean coersion avoid computing the content of the option somehow, that would be a cool optimisation. OTOH having a separate boolean thing that's guaranteed to be cheap without magic may be nicer. also yeah agree with naming, forgot about it :) |
Exactly this.
That's what I have been thinking about for the past hour. But now my head hurts and I still have no idea how to implement that. |
Oh, I thought Maybe I misread #4853 (comment) that we were discussing about the library API. |
That's how I implemented it at first, but that resulted in the repitition I mentioned in #4853 (comment) (i.e. If it's fine that every |
Yes, and I think that's okay as a start. (Regarding the method name, We could cache the result in that object (by |
Great, then I will "rollback" to that implementation (probably later today).
Thanks for elaborating on this! |
I didn't look at the current code much, so maybe I was adding a bit to the confusion because of that, sorry 🙃 Yeah I wanted to mention (but never did) that I do recall the backend doing caching of it's own, so it's ok to call verify repeatedly and does, in fact, give purpose to that caching lol |
8a379f0
to
e3aebad
Compare
Ok(SigStatus::Good) => "good", | ||
Ok(SigStatus::Bad) => "bad", | ||
Ok(SigStatus::Unknown) => "unknown", | ||
Err(_) => "invalid", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure if we want to steamroller every error into "invalid", or if we want something more fine-grained.
SignError
defines two variants:
/// An error type for the signing/verifying operations
#[derive(Debug, Error)]
pub enum SignError {
/// The verification failed because the signature *format* was invalid.
#[error("Invalid signature")]
InvalidSignatureFormat,
/// A generic error from the backend impl.
#[error("Signing error")]
Backend(#[source] Box<dyn std::error::Error + Send + Sync>),
}
Maybe we only want to convert InvalidSignatureFormat
to "invalid" and bubble up the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure that was exactly the point of separating out the explicitly invalid variant
Or maybe not 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it rarely occurs, I would just propagate all errors. The way it was implemented would be just because we didn't have a mechanism to propagate error from template functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it rarely occurs, I would just propagate all errors. The way it was implemented would be just because we didn't have a mechanism to propagate error from template functions.
That would mean removing "invalid"
entirely, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would mean removing "invalid" entirely, correct?
Correct. A template property error will be rendered inline as <Error: ..>
. If "invalid"
is non-error, it might be too verbose. So the decision would depend on whether the error is unusual or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
fn key(&self) -> Option<String> { | ||
self.verify().map_or(None, |verification| verification.key) | ||
} | ||
|
||
fn display(&self) -> Option<String> { | ||
self.verify() | ||
.map_or(None, |verification| verification.display) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe better to propagate error?
/// Is `None` when no backend was found that could read the signature. | ||
/// | ||
/// Always set by the signer. | ||
backend: Option<String>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is supposed to be set by Signer
, I don't think Verification::new()
should have backend
argument.
I personally don't mind if it's set by the signing backend, and the field is pub backend: String
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, I recall that I was a bit unsure about this part.
If the Signer
should set backend
, wouldn't we need some sort of setter method then?
Or how would we set it from "outside" Verification
?
@necauqua, @julienvincent, maybe you remember your intentions? (I think the original changes came from you?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afair no specific reason, Yujas suggestion is totally ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Signer
does set backend
here. However, removing backend
from Verification::new()
makes some tests in lib/tests/test_signing.rs
fail:
Local changes I applied to get the test results
diff --git a/lib/src/gpg_signing.rs b/lib/src/gpg_signing.rs
index 7b64c2ed11..a878fc4621 100644
--- a/lib/src/gpg_signing.rs
+++ b/lib/src/gpg_signing.rs
@@ -70,7 +70,7 @@
.next()
.and_then(|bs| str::from_utf8(bs).ok())
.map(|value| value.trim().to_owned());
- Some(Verification::new(status, key, display, Some("gpg".into())))
+ Some(Verification::new(status, key, display))
})
.ok_or(SignError::InvalidSignatureFormat)
}
@@ -223,12 +223,7 @@
fn gpg_verify_bad_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] BADSIG 123 456", true).unwrap(),
- Verification::new(
- SigStatus::Bad,
- Some("123".into()),
- Some("456".into()),
- Some("gpg".into())
- )
+ Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into()),)
);
}
@@ -236,12 +231,7 @@
fn gpg_verify_unknown_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] NO_PUBKEY 123", true).unwrap(),
- Verification::new(
- SigStatus::Unknown,
- Some("123".into()),
- None,
- Some("gpg".into())
- )
+ Verification::new(SigStatus::Unknown, Some("123".into()), None,)
);
}
@@ -249,12 +239,7 @@
fn gpg_verify_good_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] GOODSIG 123 456", true).unwrap(),
- Verification::new(
- SigStatus::Good,
- Some("123".into()),
- Some("456".into()),
- Some("gpg".into())
- )
+ Verification::new(SigStatus::Good, Some("123".into()), Some("456".into()),)
);
}
@@ -262,22 +247,12 @@
fn gpg_verify_expired_signature() {
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", true).unwrap(),
- Verification::new(
- SigStatus::Good,
- Some("123".into()),
- Some("456".into()),
- Some("gpg".into())
- )
+ Verification::new(SigStatus::Good, Some("123".into()), Some("456".into()),)
);
assert_eq!(
parse_gpg_verify_output(b"[GNUPG:] EXPKEYSIG 123 456", false).unwrap(),
- Verification::new(
- SigStatus::Bad,
- Some("123".into()),
- Some("456".into()),
- Some("gpg".into())
- )
+ Verification::new(SigStatus::Bad, Some("123".into()), Some("456".into()),)
);
}
}
diff --git a/lib/src/signing.rs b/lib/src/signing.rs
index 8f88639489..168554da2a 100644
--- a/lib/src/signing.rs
+++ b/lib/src/signing.rs
@@ -73,17 +73,12 @@
}
/// Create a new verification
- pub fn new(
- status: SigStatus,
- key: Option<String>,
- display: Option<String>,
- backend: Option<String>,
- ) -> Self {
+ pub fn new(status: SigStatus, key: Option<String>, display: Option<String>) -> Self {
Self {
status,
key,
display,
- backend,
+ backend: None,
}
}
diff --git a/lib/src/ssh_signing.rs b/lib/src/ssh_signing.rs
index 88da6d5f4d..e19f489fbf 100644
--- a/lib/src/ssh_signing.rs
+++ b/lib/src/ssh_signing.rs
@@ -251,12 +251,7 @@
Ok(_) => SigStatus::Good,
Err(_) => SigStatus::Bad,
};
- Ok(Verification::new(
- status,
- None,
- Some(principal),
- Some(self.name().into()),
- ))
+ Ok(Verification::new(status, None, Some(principal)))
}
_ => {
command
@@ -274,14 +269,8 @@
SigStatus::Unknown,
None,
Some("Signature OK. Unknown principal".into()),
- Some(self.name().into()),
- )),
- Err(_) => Ok(Verification::new(
- SigStatus::Bad,
- None,
- None,
- Some(self.name().into()),
- )),
+ )),
+ Err(_) => Ok(Verification::new(SigStatus::Bad, None, None)),
}
}
}
diff --git a/lib/src/test_signing_backend.rs b/lib/src/test_signing_backend.rs
index 71e5f3eb25..59b703f482 100644
--- a/lib/src/test_signing_backend.rs
+++ b/lib/src/test_signing_backend.rs
@@ -60,19 +60,9 @@
let sig = self.sign(data, key.as_deref())?;
if sig == signature {
- Ok(Verification::new(
- SigStatus::Good,
- key,
- Some("test".into()),
- Some(self.name().into()),
- ))
+ Ok(Verification::new(SigStatus::Good, key, Some("test".into())))
} else {
- Ok(Verification::new(
- SigStatus::Bad,
- key,
- Some("test".into()),
- Some(self.name().into()),
- ))
+ Ok(Verification::new(SigStatus::Bad, key, Some("test".into())))
}
}
}
diff --git a/lib/tests/test_signing.rs b/lib/tests/test_signing.rs
index 302bdab832..bf140a0ba7 100644
--- a/lib/tests/test_signing.rs
+++ b/lib/tests/test_signing.rs
@@ -49,7 +49,6 @@
SigStatus::Good,
Some("impeccable".to_owned()),
Some("test".into()),
- Some("test".into()),
))
}
running 6 tests
test test_signing::manual::local_backend ... FAILED
test test_signing::forced::git_backend ... FAILED
test test_signing::configured::git_backend ... FAILED
test test_signing::manual::git_backend ... FAILED
test test_signing::keep_on_rewrite::git_backend ... FAILED
test test_signing::manual_drop_on_rewrite::git_backend ... ok
failures:
---- test_signing::manual::local_backend stdout ----
thread 'test_signing::manual::local_backend' panicked at lib/tests/test_signing.rs:80:5:
assertion `left == right` failed
left: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: Some("test") })
right: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: None })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
---- test_signing::forced::git_backend stdout ----
thread 'test_signing::forced::git_backend' panicked at lib/tests/test_signing.rs:162:5:
assertion `left == right` failed
left: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: Some("test") })
right: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: None })
---- test_signing::configured::git_backend stdout ----
thread 'test_signing::configured::git_backend' panicked at lib/tests/test_signing.rs:181:5:
assertion `left == right` failed
left: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: Some("test") })
right: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: None })
---- test_signing::manual::git_backend stdout ----
thread 'test_signing::manual::git_backend' panicked at lib/tests/test_signing.rs:80:5:
assertion `left == right` failed
left: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: Some("test") })
right: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: None })
---- test_signing::keep_on_rewrite::git_backend stdout ----
thread 'test_signing::keep_on_rewrite::git_backend' panicked at lib/tests/test_signing.rs:109:5:
assertion `left == right` failed
left: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: Some("test") })
right: Some(Verification { status: Good, key: Some("impeccable"), display: Some("test"), backend: None })
failures:
test_signing::configured::git_backend
test_signing::forced::git_backend
test_signing::keep_on_rewrite::git_backend
test_signing::manual::git_backend
test_signing::manual::local_backend
test result: FAILED. 1 passed; 5 failed; 0 ignored; 0 measured; 396 filtered out; finished in 0.20s
error: test failed, to rerun pass `-p jj-lib --test runner`
We can no longer construct a Verification
instance to assert against (see good_signature()
).
Since nobody raised any major concerns with it, I will just go for @yuja's suggestion of making Verification::backend
public since that is easy to do.
We could convert the failing test_signing
tests to snapshot tests, which would let us assert without having to construct a Verification
instance (like we do in test_gpg
). I am happy to go that extra mile, if you confirm that it's worth it! 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eh, you could fix good_signature with the field being public, or you could convert the tests if you want, not a huge difference in terms of worth idk
We need to make `TestSigningBackend` available for cli tests, as we will add cli tests for signing related functionality (templates for displaying commit signatures, `jj sign`) in upcoming commits. Co-authored-by: julienvincent <[email protected]>
We need to provide a value for the `display` attribute to assert against in upcoming tests, which verify signature templates.
e3aebad
to
8eef259
Compare
Ok(SigStatus::Good) => "good", | ||
Ok(SigStatus::Bad) => "bad", | ||
Ok(SigStatus::Unknown) => "unknown", | ||
Err(_) => "invalid", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would mean removing "invalid" entirely, correct?
Correct. A template property error will be rendered inline as <Error: ..>
. If "invalid"
is non-error, it might be too verbose. So the decision would depend on whether the error is unusual or not.
e502a68
to
a4ba8f8
Compare
Disclaimer: this is the work of @necauqua and @julienvincent (see jj-vcs#3141). I simply materialized the changes by rebasing them on latest `main` and making the necessary adjustments to pass CI. --- I had to fix an issue in `TestSignatureBackend::sign()`. The following test was failing: ``` ---- test_signature_templates::test_signature_templates stdout ---- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Snapshot Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Snapshot: signature_templates Source: cli/tests/test_signature_templates.rs:28 ──────────────────────────────────────────────────────────────────────────────── Expression: stdout ──────────────────────────────────────────────────────────────────────────────── -old snapshot +new results ────────────┬─────────────────────────────────────────────────────────────────── 0 0 │ @ Commit ID: 05ac066d05701071af20e77506a0f2195194cbc9 1 1 │ │ Change ID: qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu 2 2 │ │ Author: Test User <[email protected]> (2001-02-03 08:05:07) 3 3 │ │ Committer: Test User <[email protected]> (2001-02-03 08:05:07) 4 │-│ Signature: Good test signature 4 │+│ Signature: Bad test signature 5 5 │ │ 6 6 │ │ (no description set) 7 7 │ │ 8 8 │ ◆ Commit ID: 0000000000000000000000000000000000000000 ────────────┴─────────────────────────────────────────────────────────────────── ``` Print debugging revealed that the signature was bad, because of a missing trailing `\n` in `TestSignatureBackend::sign()`. ```diff diff --git a/lib/src/test_signing_backend.rs b/lib/src/test_signing_backend.rs index d47fef1086..0ba249e358 100644 --- a/lib/src/test_signing_backend.rs +++ b/lib/src/test_signing_backend.rs @@ -59,6 +59,8 @@ let key = (!key.is_empty()).then_some(std::str::from_utf8(key).unwrap().to_owned()); let sig = self.sign(data, key.as_deref())?; + dbg!(&std::str::from_utf8(&signature).unwrap()); + dbg!(&std::str::from_utf8(&sig).unwrap()); if sig == signature { Ok(Verification::new( SigStatus::Good, ``` ``` [lib/src/test_signing_backend.rs:62:9] &std::str::from_utf8(&signature).unwrap() = \"--- JJ-TEST-SIGNATURE ---\\nKEY: \\n5300977ff3ecda4555bd86d383b070afac7b7459c07f762af918943975394a8261d244629e430c8554258904f16dd9c18d737f8969f2e7d849246db0d93cc004\\n\" [lib/src/test_signing_backend.rs:63:9] &std::str::from_utf8(&sig).unwrap() = \"--- JJ-TEST-SIGNATURE ---\\nKEY: \\n5300977ff3ecda4555bd86d383b070afac7b7459c07f762af918943975394a8261d244629e430c8554258904f16dd9c18d737f8969f2e7d849246db0d93cc004\" ``` Thankfully, @yuja pointed out that libgit2 appends a trailing newline (see bfb7613). Co-authored-by: necauqua <[email protected]> Co-authored-by: julienvincent <[email protected]>
a4ba8f8
to
d76a54d
Compare
Calling any of `.status()`, `.key()`, or `.display()` is slow, as it incurs | ||
the performance cost of verifying the signature. Though consecutive calls | ||
will be faster, because the backend caches the verification result. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(actually looking through the code top-to-bottom finally)
imo you should mention that just checking for signature presence via the boolean coersion (if(commit.signature(), "commit has a signature", "commit is unsigned")
) is very fast and cheap, as opposed to the verification part
Also, [..] verifying the signature. Though [..]
-> [..] verifying the signature (for example, shelling out to gpg or ssh-keygen). Though [..]
, just giving an example of why it is slow.
fyi creating a single commit with a gpg signature made through yubikey takes ~200ms for me, a bit annoying :)
fyi² it is (by design) possible to implement another gpg backend (for example) that'll be faster by using some rust gpg implementation or something instead of the lazy way of just calling the gpg binary.
* Add templater support for rendering commit signatures. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reminder: you'll need to relocate the changelog entry after the release
lib/src/test_signing_backend.rs
Outdated
@@ -63,13 +63,13 @@ impl SigningBackend for TestSigningBackend { | |||
Ok(Verification { | |||
status: SigStatus::Good, | |||
key, | |||
display: None, | |||
display: Some(self.name().into()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: squash Some("test".into())
from the next commit
These changes are continuing the work of #3141.
I took over its changes and rebased them on latest
main
.This work is required for #4747, as it lays the foundation for writing CLI tests for
jj sign
.Checklist
If applicable:
CHANGELOG.md
I have updated the config schema (cli/src/config-schema.json)