diff --git a/Cargo.lock b/Cargo.lock index 5e001be..ab7bd23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "4bde65b3c84000288c0abe8aa601a4b7c40b0dbbb7d144dd6c712ed9796e1fd5" dependencies = [ "blowfish", "hex-literal", - "pbkdf2", + "pbkdf2 0.10.1", "sha2 0.10.8", ] @@ -106,10 +106,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab" dependencies = [ "byteorder", - "cipher", + "cipher 0.3.0", "opaque-debug", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -142,6 +148,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.23" @@ -207,6 +223,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b916ba8ce9e4182696896f015e8a5ae6081b305f74690baa8465e35f5a142ea4" + [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -292,6 +314,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "git-signify" version = "0.3.0" @@ -300,6 +335,7 @@ dependencies = [ "clap", "git2", "libsignify", + "minisign", "rpassword", "zeroize", ] @@ -331,6 +367,15 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -470,6 +515,15 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -485,6 +539,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.169" @@ -556,6 +620,24 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "minisign" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26541387415a1e829df5d532aad019fb11bc723e2b5bc99edefa4cf5bfad0de7" +dependencies = [ + "ct-codecs", + "getrandom", + "rpassword", + "scrypt", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -589,6 +671,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -685,6 +777,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2 0.12.2", + "salsa20", + "sha2 0.10.8", +] + [[package]] name = "serde" version = "1.0.217" @@ -850,6 +962,66 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index b5be8f1..672ca18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = "1.0.95" clap = { version = "4.5.23", features = ["derive", "env"] } git2 = "0.19.0" +minisign = "0.7.9" rpassword = "7.3.1" zeroize = "1.8.1" diff --git a/src/raw/sign.rs b/src/raw/sign.rs index a652888..2597f2b 100644 --- a/src/raw/sign.rs +++ b/src/raw/sign.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use anyhow::{Context, Result}; -use git2::{Oid, Repository}; +use git2::{ObjectType, Oid, Repository}; use crate::utils; @@ -19,28 +19,47 @@ 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: &utils::PrivateKey, rev: &str) -> Result { - let oid = repo + let object = repo .revparse_single(rev) - .context("Failed to look-up git object id")? - .id(); + .context("Failed to look-up git object id")?; - let object_blob = repo - .blob(oid.as_bytes()) - .context("Failed to write object id to the git store")?; + let object_ptr = object.id(); + let object_mode = match object + .kind() + .context("Failed to determine object kind to sign")? + { + ObjectType::Blob => 0o100644, + ObjectType::Tree => 0o040000, + ty @ (ObjectType::Any | ObjectType::Commit | ObjectType::Tag) => { + anyhow::bail!("Unsupported object type {ty}"); + } + }; - let signature = secret_key.sign(oid.as_bytes()); + let signature = secret_key.sign(object_ptr.as_bytes())?; let signature_blob = repo .blob(&signature) .context("Failed to write signature to the object store")?; + let version_blob = repo + .blob(utils::TreeSignatureVersion::current().as_str().as_bytes()) + .context("Failed to write tree signature version to the object store")?; + + let algo_blob = repo + .blob(secret_key.algorithm().as_str().as_bytes()) + .context("Failed to write tree signature algorithm to the object store")?; + let mut tree_builder = repo .treebuilder(None) .context("Failed to get a git tree object builder")?; - // TODO: insert a tree entry containing the version of this program - tree_builder - .insert("object", object_blob, 0o100644) + .insert("version", version_blob, 0o100644) + .context("Failed to write version to the tree")?; + tree_builder + .insert("algorithm", algo_blob, 0o100644) + .context("Failed to write algorithm to the tree")?; + tree_builder + .insert("object", object_ptr, object_mode) .context("Failed to write object to the tree")?; tree_builder .insert("signature", signature_blob, 0o100644) diff --git a/src/sign.rs b/src/sign.rs index 72ff813..66ffc13 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 = secret_key.public_key().fingerprint()?; + 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 f48336c..909124e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,31 +2,54 @@ use std::error; use std::fmt; +use std::io::Cursor; use std::path::PathBuf; use anyhow::{anyhow, Context, Result}; -use git2::{Blob, ObjectType, Oid, Repository, RepositoryOpenFlags}; -use libsignify::{Codeable, Signature}; +use git2::{Blob, Object, ObjectType, Oid, Repository, RepositoryOpenFlags}; +use libsignify::Codeable; use zeroize::Zeroizing; /// Private key used to sign git objects. pub enum PrivateKey { /// Private key originating from [`libsignify`]. Signify(libsignify::PrivateKey), + /// Private key originating from [`minisign`]. + Minisign(minisign::SecretKey), } impl PrivateKey { /// Return the [`PublicKey`] associated with this [`PrivateKey`]. - pub fn public_key(&self) -> PublicKey { + pub fn public_key(&self) -> Result { match self { - Self::Signify(private_key) => PublicKey::Signify(private_key.public()), + Self::Signify(private_key) => Ok(PublicKey::Signify(private_key.public())), + Self::Minisign(private_key) => Ok(PublicKey::Minisign( + minisign::PublicKey::from_secret_key(private_key) + .context("Failed to convert minisign private key to public key")?, + )), } } /// Sign a message using the given private key. - pub fn sign>(&self, msg: T) -> Vec { + pub fn sign>(&self, msg: T) -> Result> { match self { - Self::Signify(private_key) => private_key.sign(msg.as_ref()).as_bytes(), + Self::Signify(private_key) => Ok(private_key + .sign(msg.as_ref()) + .to_file_encoding("signed with git-signify via libsignify")), + Self::Minisign(private_key) => { + let signature_box = + minisign::sign(None, private_key, Cursor::new(msg.as_ref()), None, None) + .context("Failed to sign git object with minisign private key")?; + Ok(String::from(signature_box).into_bytes()) + } + } + } + + /// Return the algorithm of this [`PrivateKey`]. + pub const fn algorithm(&self) -> TreeSignatureAlgo { + match self { + Self::Signify(_) => TreeSignatureAlgo::Signify, + Self::Minisign(_) => TreeSignatureAlgo::Minisign, } } } @@ -35,15 +58,18 @@ impl PrivateKey { pub enum PublicKey { /// Public key originating from [`libsignify`]. Signify(libsignify::PublicKey), + /// Public key originating from [`minisign`]. + Minisign(minisign::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") - } + Self::Signify(public_key) => hash_bytes(public_key.key()) + .context("Failed to compute signify public key fingerprint"), + Self::Minisign(public_key) => hash_bytes(public_key.to_bytes()) + .context("Failed to compute minisign public key fingerprint"), } } } @@ -52,12 +78,43 @@ impl PublicKey { pub enum TreeSignatureVersion { /// Version 0 tree signatures. V0, + /// Version 1 tree signatures. + V1, +} + +impl TreeSignatureVersion { + /// Parse a [`TreeSignatureVersion`] from a git [`Blob`]. + pub fn from_blob(blob: Blob<'_>) -> Result { + match blob.content() { + b"v0" => Ok(Self::V0), + b"v1" => Ok(Self::V1), + blob => Err(anyhow!( + "Invalid tree signature version {:?}", + String::from_utf8_lossy(blob) + )), + } + } + + /// Return the current version. + pub const fn current() -> Self { + TreeSignatureVersion::V1 + } + + /// Encode the version as a string. + pub const fn as_str(&self) -> &str { + match self { + Self::V0 => "v0", + Self::V1 => "v1", + } + } } /// Enumeration of all possible algorithms of a [`TreeSignature`]. pub enum TreeSignatureAlgo { /// Signify key. Signify, + /// Minisign key. + Minisign, } impl TreeSignatureAlgo { @@ -65,40 +122,31 @@ impl TreeSignatureAlgo { pub fn from_blob(blob: Blob<'_>) -> Result { match blob.content() { b"signify" => Ok(Self::Signify), + b"minisign" => Ok(Self::Minisign), 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 version {:?}", - String::from_utf8_lossy(blob.content()) - )) - } - /// Return the current version. - #[allow(dead_code)] - pub const fn current() -> Self { - TreeSignatureVersion::V0 + /// Encode the algorithm as a string. + pub const fn as_str(&self) -> &str { + match self { + Self::Signify => "signify", + Self::Minisign => "minisign", + } } } /// A signature stored in a git tree object. pub struct TreeSignature<'repo> { /// Version of the tree signature. - #[allow(dead_code)] 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>, + pub object_pointer: Object<'repo>, /// The signature over the git object. pub signature: Blob<'repo>, } @@ -153,15 +201,11 @@ impl<'repo> TreeSignature<'repo> { }, )?; - let object = tree + let object_pointer = tree .get_name("object") .context("Failed to look-up signed object in the tree")? .to_object(repo) .context("The signed object could not be retrieved")?; - let object_pointer = match object.into_blob() { - Ok(ptr) => ptr, - Err(_) => return Err(anyhow!("The signed object is not a blob")), - }; let signature = { let signature = tree @@ -184,18 +228,79 @@ impl<'repo> TreeSignature<'repo> { /// Verify the authenticity of this [`TreeSignature`]. pub fn verify(&self, public_key: &PublicKey) -> Result<()> { - 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")?; + self.check_compatibility(public_key) + .context("Incompatible public key provided")?; + + match public_key { + PublicKey::Signify(public_key) => { + let signature = match &self.version { + TreeSignatureVersion::V0 => { + libsignify::Signature::from_bytes(self.signature.content()) + .map_err(Error::new) + .context("Failed to parse signify signature from git blob")? + } + TreeSignatureVersion::V1 => { + let signature_content = std::str::from_utf8(self.signature.content()) + .context("Found non-utf8 data in signify signature content")?; + + let (signature, _) = libsignify::Signature::from_base64(signature_content) + .map_err(Error::new) + .context("Failed to parse signify signature from git blob")?; + + signature + } + }; - let dereferenced_obj = self.object_pointer.content(); + let dereferenced_obj = self.dereference()?; public_key - .verify(dereferenced_obj, &signature) + .verify(dereferenced_obj.as_bytes(), &signature) .map_err(Error::new) - .context("Failed to verify signature") + .context("Invalid signify signature") + } + PublicKey::Minisign(public_key) => { + let signature_box = match &self.version { + TreeSignatureVersion::V0 => { + anyhow::bail!("minisign public keys not supported in v0"); + } + TreeSignatureVersion::V1 => { + let signature_content = std::str::from_utf8(self.signature.content()) + .context("Found non-utf8 data in minisign signature content")?; + + minisign::SignatureBox::from_string(signature_content) + .context("Failed to parse minisign signature from git blob")? + } + }; + + let dereferenced_obj = self.dereference()?; + + minisign::verify( + public_key, + &signature_box, + Cursor::new(dereferenced_obj.as_bytes()), + true, + false, + false, + ) + .context("Invalid minisign signature") + } + } + } + + /// Check the compatibility of the given public key with this + /// tree signature. + pub fn check_compatibility(&self, key: &PublicKey) -> Result<()> { + match (&self.version, &self.algorithm, key) { + (TreeSignatureVersion::V0, TreeSignatureAlgo::Signify, PublicKey::Signify(_)) + | (TreeSignatureVersion::V1, TreeSignatureAlgo::Signify, PublicKey::Signify(_)) + | (TreeSignatureVersion::V1, TreeSignatureAlgo::Minisign, PublicKey::Minisign(_)) => { + Ok(()) + } + _ => { + anyhow::bail!( + "Attempted to validate signature with a public key of an incompatible \ + type" + ); } } } @@ -203,7 +308,17 @@ impl<'repo> TreeSignature<'repo> { /// Dereference the inner object pointer. #[inline] pub fn dereference(&self) -> Result { - Oid::from_bytes(self.object_pointer.content()).context("Failed to parse git object id") + match &self.version { + TreeSignatureVersion::V0 => { + let blob = self + .object_pointer + .as_blob() + .context("The signed object is not a blob")?; + let oid_bytes = blob.content(); + Oid::from_bytes(oid_bytes).context("Failed to parse git object id from raw bytes") + } + TreeSignatureVersion::V1 => Ok(self.object_pointer.id()), + } } } @@ -245,9 +360,7 @@ fn determine_key_format(key_data: &str) -> Result { match rest { s if s.starts_with("signify") => Ok(TreeSignatureAlgo::Signify), - s if s.starts_with("minisign") => { - todo!("minisign keys aren't supported yet") - } + s if s.starts_with("minisign") => Ok(TreeSignatureAlgo::Minisign), _ => Err(anyhow!("Unknown key format")), } } @@ -260,10 +373,20 @@ pub fn get_public_key(path: PathBuf) -> Result { TreeSignatureAlgo::Signify => { let (public_key, _) = libsignify::PublicKey::from_base64(&key_data[..]) .map_err(Error::new) - .context("Failed to decode public key")?; + .context("Failed to decode signify public key")?; PublicKey::Signify(public_key) } + TreeSignatureAlgo::Minisign => { + let public_key = minisign::PublicKeyBox::from_string(&key_data[..]) + .context("Failed to read minisign public key")?; + + PublicKey::Minisign( + public_key + .into_public_key() + .context("Failed to decode minisign public key")?, + ) + } }) } @@ -280,9 +403,7 @@ pub fn get_secret_key(path: PathBuf) -> Result { .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")?; + let passphrase = prompt_key_passphrase().map(Zeroizing::new)?; secret_key .decrypt_with_password(&passphrase) @@ -292,9 +413,25 @@ pub fn get_secret_key(path: PathBuf) -> Result { PrivateKey::Signify(secret_key) } + TreeSignatureAlgo::Minisign => { + let private_key = minisign::SecretKeyBox::from_string(&key_data[..]) + .context("Failed to read minisign secret key")?; + + let passphrase = prompt_key_passphrase()?; + + PrivateKey::Minisign( + private_key + .into_secret_key(Some(passphrase)) + .context("Failed to decode minisign private key")?, + ) + } }) } +fn prompt_key_passphrase() -> Result { + rpassword::prompt_password("key passphrase: ").context("Failed to read secret key passphrase") +} + /// Try to find and open a git repository. pub fn open_repository() -> Result { Repository::open_ext(