crates/cli/ents-lens/src/signing.rs
signing.rshistorycomment on this file
| 1 | //! The signing identity the composition root injects into the lens |
| 2 | //! (`lens.serve`, `roots.web-agnostic`). |
| 3 | //! |
| 4 | //! The lens writes new comments through the same signed mutation path |
| 5 | //! every other frontend uses (`lens.parity`), so it must be handed an |
| 6 | //! identity to sign with — but, exactly like `ents-web`, it resolves no |
| 7 | //! key itself and assumes nothing about which editor (if any) is attached. |
| 8 | //! [`Signing`] is a plain owned value the root builds once and moves in; |
| 9 | //! there is no second implementation to abstract over the way `ents-web`'s |
| 10 | //! hosted/local split needs, because a lens only ever serves the local |
| 11 | //! root (`lens.serve`), so a concrete carrier is enough and no trait is |
| 12 | //! introduced. |
| 13 | |
| 14 | /// A closure that signs a commit's to-be-signed bytes, producing an armored |
| 15 | /// SSHSIG PEM block — the injected half of [`Signing`]. |
| 16 | pub type SignFn = Box<dyn Fn(&[u8]) -> String>; |
| 17 | |
| 18 | /// An owned signing identity: the commit author signature and a closure |
| 19 | /// that produces an SSHSIG armored block for a commit's bytes, plus the |
| 20 | /// public key that identifies the acting member. |
| 21 | /// |
| 22 | /// Built by the composition root from the user's own key (the same |
| 23 | /// resolution `git ents comment` and `git ents serve` perform) and moved |
| 24 | /// into the [`crate::Lens`]; the lens never resolves a key path or reads |
| 25 | /// `user.signingkey` itself. |
| 26 | /// |
| 27 | /// # Examples |
| 28 | /// |
| 29 | /// ``` |
| 30 | /// use ents_lens::Signing; |
| 31 | /// |
| 32 | /// let signing = Signing::new( |
| 33 | /// gix::actor::Signature { |
| 34 | /// name: "jdc".into(), |
| 35 | /// email: "jdc@ents.test".into(), |
| 36 | /// time: gix::date::Time { seconds: 0, offset: 0 }, |
| 37 | /// }, |
| 38 | /// Box::new(|_payload| "-----BEGIN SSH SIGNATURE-----\n-----END SSH SIGNATURE-----\n".to_owned()), |
| 39 | /// "ssh-ed25519 AAAA... jdc".to_owned(), |
| 40 | /// ); |
| 41 | /// assert_eq!(signing.actor().name, "jdc"); |
| 42 | /// ``` |
| 43 | pub struct Signing { |
| 44 | actor: gix::actor::Signature, |
| 45 | sign: SignFn, |
| 46 | public_openssh: String, |
| 47 | } |
| 48 | |
| 49 | impl Signing { |
| 50 | /// Build a signing identity from an already-resolved key: the commit |
| 51 | /// `actor` signature, a `sign` closure over the key, and the key's |
| 52 | /// `public_openssh` single-line form. |
| 53 | #[must_use] |
| 54 | pub fn new(actor: gix::actor::Signature, sign: SignFn, public_openssh: String) -> Self { |
| 55 | Self { |
| 56 | actor, |
| 57 | sign, |
| 58 | public_openssh, |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// The commit author/committer signature every comment mutation this |
| 63 | /// identity signs will carry. |
| 64 | #[must_use] |
| 65 | pub fn actor(&self) -> gix::actor::Signature { |
| 66 | self.actor.clone() |
| 67 | } |
| 68 | |
| 69 | /// The public half of this identity's key, in OpenSSH single-line |
| 70 | /// form — which enrolled member is acting. |
| 71 | #[must_use] |
| 72 | pub fn public_openssh(&self) -> &str { |
| 73 | &self.public_openssh |
| 74 | } |
| 75 | |
| 76 | /// Sign `payload` (a commit's to-be-signed bytes), returning the |
| 77 | /// armored SSHSIG PEM block for the commit's `gpgsig` header. |
| 78 | #[must_use] |
| 79 | pub fn sign(&self, payload: &[u8]) -> String { |
| 80 | (self.sign)(payload) |
| 81 | } |
| 82 | } |