diff --git a/src/fingerprint.rs b/src/fingerprint.rs index 9fd58c0..974a203 100644 --- a/src/fingerprint.rs +++ b/src/fingerprint.rs @@ -9,7 +9,7 @@ use super::utils; /// Execute the `fingerprint` command. pub fn command(key_path: PathBuf) -> Result<()> { let public_key = utils::get_public_key(key_path)?; - let hash = utils::hash_bytes(public_key.key().as_ref())?; + let hash = public_key.fingerprint()?; println!("{hash}"); Ok(()) } diff --git a/src/raw/sign.rs b/src/raw/sign.rs index 4bad289..a652888 100644 --- a/src/raw/sign.rs +++ b/src/raw/sign.rs @@ -4,7 +4,6 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use git2::{Oid, Repository}; -use libsignify::{Codeable, PrivateKey}; use crate::utils; @@ -19,7 +18,7 @@ pub fn command(key_path: PathBuf, rev: String) -> Result<()> { /// Sign the revision `rev` with the given secret key, write the results /// to `repo` and return the object id of the resulting signature tree. -pub fn sign(repo: &Repository, secret_key: &PrivateKey, rev: &str) -> Result { +pub fn sign(repo: &Repository, secret_key: &utils::PrivateKey, rev: &str) -> Result { let oid = repo .revparse_single(rev) .context("Failed to look-up git object id")? @@ -29,7 +28,7 @@ pub fn sign(repo: &Repository, secret_key: &PrivateKey, rev: &str) -> Result Result<()> /// Verify the signature under `tree_rev` with the given public key. pub fn verify( repo: &Repository, - public_key: &PublicKey, + public_key: &utils::PublicKey, tree_rev: &str, recover: bool, ) -> Result> { diff --git a/src/sign.rs b/src/sign.rs index 13cffc3..72ff813 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -17,7 +17,7 @@ pub fn command(key_path: PathBuf, rev: String) -> Result<()> { let tree_sig = utils::TreeSignature::load_oid(&repo, tree_oid)?; tree_sig.dereference()? }; - let key_fingerprint = utils::hash_bytes(&secret_key.public().key()[..])?; + let key_fingerprint = secret_key.public_key().fingerprint()?; let reference = utils::craft_signature_reference(key_fingerprint, signed_object); repo.reference( &reference, tree_oid, diff --git a/src/utils.rs b/src/utils.rs index b356367..f48336c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,40 +6,101 @@ use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; use git2::{Blob, ObjectType, Oid, Repository, RepositoryOpenFlags}; -use libsignify::{Codeable, PrivateKey, PublicKey, Signature}; +use libsignify::{Codeable, Signature}; use zeroize::Zeroizing; -/// Enumeration of all possible formats of a [`TreeSignature`]. -pub enum TreeSignatureFormat { +/// Private key used to sign git objects. +pub enum PrivateKey { + /// Private key originating from [`libsignify`]. + Signify(libsignify::PrivateKey), +} + +impl PrivateKey { + /// Return the [`PublicKey`] associated with this [`PrivateKey`]. + pub fn public_key(&self) -> PublicKey { + match self { + Self::Signify(private_key) => PublicKey::Signify(private_key.public()), + } + } + + /// Sign a message using the given private key. + pub fn sign>(&self, msg: T) -> Vec { + match self { + Self::Signify(private_key) => private_key.sign(msg.as_ref()).as_bytes(), + } + } +} + +/// Public key used to verify signed git objects. +pub enum PublicKey { + /// Public key originating from [`libsignify`]. + Signify(libsignify::PublicKey), +} + +impl PublicKey { + /// Compute the fingerprint of the given public key. + pub fn fingerprint(&self) -> Result { + match self { + Self::Signify(public_key) => { + hash_bytes(public_key.key()).context("Failed to compute public key fingerprint") + } + } + } +} + +/// Enumeration of all possible versions of a [`TreeSignature`]. +pub enum TreeSignatureVersion { /// Version 0 tree signatures. V0, } -impl TreeSignatureFormat { - /// Parse a [`TreeSignatureFormat`] from a git [`Blob`]. +/// Enumeration of all possible algorithms of a [`TreeSignature`]. +pub enum TreeSignatureAlgo { + /// Signify key. + Signify, +} + +impl TreeSignatureAlgo { + /// Parse a [`TreeSignatureAlgo`] from a git [`Blob`]. + pub fn from_blob(blob: Blob<'_>) -> Result { + match blob.content() { + b"signify" => Ok(Self::Signify), + blob => Err(anyhow!( + "Invalid tree signature algorithm {:?}", + String::from_utf8_lossy(blob) + )), + } + } +} + +impl TreeSignatureVersion { + /// Parse a [`TreeSignatureVersion`] from a git [`Blob`]. pub fn from_blob(blob: Blob<'_>) -> Result { Err(anyhow!( - "Invalid tree signature format {:?}", + "Invalid tree signature version {:?}", String::from_utf8_lossy(blob.content()) )) } - /// Return the current format. + /// Return the current version. #[allow(dead_code)] pub const fn current() -> Self { - TreeSignatureFormat::V0 + TreeSignatureVersion::V0 } } /// A signature stored in a git tree object. pub struct TreeSignature<'repo> { - /// Format of the tree signature. + /// Version of the tree signature. #[allow(dead_code)] - pub format: TreeSignatureFormat, + pub version: TreeSignatureVersion, + /// Algorithm of the tree signature. + #[allow(dead_code)] + pub algorithm: TreeSignatureAlgo, /// Pointer to the object that was signed. pub object_pointer: Blob<'repo>, /// The signature over the git object. - pub signature: Signature, + pub signature: Blob<'repo>, } impl<'repo> TreeSignature<'repo> { @@ -61,20 +122,36 @@ impl<'repo> TreeSignature<'repo> { .find_tree(oid) .context("No tree object found for the given revision")?; - let format = - tree.get_name("format") - .map_or(Ok(TreeSignatureFormat::V0), |format_tree_entry| { - let format_obj = format_tree_entry - .to_object(repo) - .context("The tree signature format could not be retrieved")?; - let format_blob = match format_obj.into_blob() { - Ok(ptr) => ptr, - Err(_) => { - return Err(anyhow!("The tree signature format object is not a blob")) - } - }; - TreeSignatureFormat::from_blob(format_blob) - })?; + let (version, algorithm) = tree.get_name("version").map_or( + Ok((TreeSignatureVersion::V0, TreeSignatureAlgo::Signify)), + |version_tree_entry| { + let version_obj = version_tree_entry + .to_object(repo) + .context("The tree signature version could not be retrieved")?; + let version_blob = match version_obj.into_blob() { + Ok(blob) => blob, + Err(_) => { + return Err(anyhow!("The tree signature version object is not a blob")) + } + }; + let version = TreeSignatureVersion::from_blob(version_blob)?; + + let algorithm_obj = tree + .get_name("algorithm") + .context("Failed to look-up tree signature algorithm")? + .to_object(repo) + .context("The tree signature algorithm could not be retrieved")?; + let algorithm_blob = match algorithm_obj.into_blob() { + Ok(blob) => blob, + Err(_) => { + return Err(anyhow!("The tree signature algorithm object is not a blob")) + } + }; + let algorithm = TreeSignatureAlgo::from_blob(algorithm_blob)?; + + anyhow::Ok((version, algorithm)) + }, + )?; let object = tree .get_name("object") @@ -92,16 +169,14 @@ impl<'repo> TreeSignature<'repo> { .context("Failed to look-up signature in the tree")? .to_object(repo) .context("The signature object could not be retrieved")?; - let signature = signature - .as_blob() - .context("The signature object is not a blob")?; - Signature::from_bytes(signature.content()) - .map_err(Error::new) - .context("Failed to parse signature")? + signature + .into_blob() + .map_err(|_| anyhow!("The signature object in {oid} is not a blob"))? }; Ok(Self { - format, + version, + algorithm, signature, object_pointer, }) @@ -109,11 +184,20 @@ impl<'repo> TreeSignature<'repo> { /// Verify the authenticity of this [`TreeSignature`]. pub fn verify(&self, public_key: &PublicKey) -> Result<()> { - let dereferenced_obj = self.object_pointer.content(); - public_key - .verify(dereferenced_obj, &self.signature) - .map_err(Error::new) - .context("Failed to verify signature") + match (&self.algorithm, public_key) { + (TreeSignatureAlgo::Signify, PublicKey::Signify(public_key)) => { + let signature = Signature::from_bytes(self.signature.content()) + .map_err(Error::new) + .context("Failed to parse signify signature from git blob")?; + + let dereferenced_obj = self.object_pointer.content(); + + public_key + .verify(dereferenced_obj, &signature) + .map_err(Error::new) + .context("Failed to verify signature") + } + } } /// Dereference the inner object pointer. @@ -147,45 +231,68 @@ impl Error { /// Hash the provided bytearray and return the /// resulting checksum. #[inline] -pub fn hash_bytes(bytes: &[u8]) -> Result { - Oid::hash_object(ObjectType::Blob, bytes).context("Failed to hash bytes") +fn hash_bytes>(bytes: T) -> Result { + Oid::hash_object(ObjectType::Blob, bytes.as_ref()).context("Failed to hash bytes") +} + +/// Determine the format of the given key data. +fn determine_key_format(key_data: &str) -> Result { + const UNTRUSTED_COMMENT: &str = "untrusted comment: "; + + let Some(("", rest)) = key_data.split_once(UNTRUSTED_COMMENT) else { + anyhow::bail!("Unknown key format"); + }; + + match rest { + s if s.starts_with("signify") => Ok(TreeSignatureAlgo::Signify), + s if s.starts_with("minisign") => { + todo!("minisign keys aren't supported yet") + } + _ => Err(anyhow!("Unknown key format")), + } } /// Read a public key from the given path. pub fn get_public_key(path: PathBuf) -> Result { let key_data = std::fs::read_to_string(path).context("Failed to read public key")?; - let (public_key, _) = PublicKey::from_base64(&key_data[..]) - .map_err(Error::new) - .context("Failed to decode public key")?; + Ok(match determine_key_format(&key_data)? { + TreeSignatureAlgo::Signify => { + let (public_key, _) = libsignify::PublicKey::from_base64(&key_data[..]) + .map_err(Error::new) + .context("Failed to decode public key")?; - Ok(public_key) + PublicKey::Signify(public_key) + } + }) } /// Read a secret key from the given path. pub fn get_secret_key(path: PathBuf) -> Result { - let (mut secret_key, _) = { - let key_data = std::fs::read_to_string(path) - .map(Zeroizing::new) - .context("Failed to read secret key")?; - - PrivateKey::from_base64(&key_data[..]) - .map_err(Error::new) - .context("Failed to decode secret key")? - }; + let key_data = std::fs::read_to_string(path) + .map(Zeroizing::new) + .context("Failed to read secret key")?; + + Ok(match determine_key_format(&key_data)? { + TreeSignatureAlgo::Signify => { + let (mut secret_key, _) = libsignify::PrivateKey::from_base64(&key_data[..]) + .map_err(Error::new) + .context("Failed to decode secret key")?; - if secret_key.is_encrypted() { - let passphrase = rpassword::prompt_password("key passphrase: ") - .map(Zeroizing::new) - .context("Failed to read secret key password")?; + if secret_key.is_encrypted() { + let passphrase = rpassword::prompt_password("key passphrase: ") + .map(Zeroizing::new) + .context("Failed to read secret key password")?; - secret_key - .decrypt_with_password(&passphrase) - .map_err(Error::new) - .context("Failed to decrypt secret key")?; - } + secret_key + .decrypt_with_password(&passphrase) + .map_err(Error::new) + .context("Failed to decrypt secret key")?; + } - Ok(secret_key) + PrivateKey::Signify(secret_key) + } + }) } /// Try to find and open a git repository. diff --git a/src/verify.rs b/src/verify.rs index 4e06e42..e8b8d70 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -17,7 +17,7 @@ pub fn command(key_path: PathBuf, rev: String) -> Result<()> { .revparse_single(&rev) .context("Failed to look-up git object")? .id(); - let key_fingerprint = utils::hash_bytes(&public_key.key()[..])?; + let key_fingerprint = public_key.fingerprint()?; utils::craft_signature_reference(key_fingerprint, object_oid) }; verify(&repo, &public_key, &tree_rev, false)?;