Harness the power of signify(1) to sign arbitrary git objects
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 700 lines 26 kB view raw
1//! Catch-all utilities module. 2 3use std::collections::BTreeMap; 4use std::error; 5use std::fmt; 6use std::fs; 7use std::io::Cursor; 8use std::path::{Path, PathBuf}; 9 10use anyhow::{anyhow, Context, Result}; 11use git2::{Blob, ErrorCode, Object, ObjectType, Oid, Repository, RepositoryOpenFlags}; 12use libsignify::Codeable; 13use ml_signify::codec::{codecs, Decode as _, Encode as _}; 14use zeroize::Zeroizing; 15 16/// Private key used to sign git objects. 17pub enum PrivateKey { 18 /// Private key originating from [`libsignify`]. 19 Signify(libsignify::PrivateKey), 20 /// Private key originating from [`minisign`]. 21 Minisign(minisign::SecretKey), 22 /// Private key originating from [`ml_signify`]. 23 MlSignify(Box<ml_signify::SigningKey>), 24} 25 26impl PrivateKey { 27 /// Return the [`PublicKey`] associated with this [`PrivateKey`]. 28 pub fn public_key(&self) -> Result<PublicKey> { 29 match self { 30 Self::Signify(private_key) => Ok(PublicKey::Signify(private_key.public())), 31 Self::Minisign(private_key) => Ok(PublicKey::Minisign( 32 minisign::PublicKey::from_secret_key(private_key) 33 .context("Failed to convert minisign private key to public key")?, 34 )), 35 Self::MlSignify(private_key) => { 36 Ok(PublicKey::MlSignify(Box::new(private_key.verifying_key()))) 37 } 38 } 39 } 40 41 /// Sign a message using the given private key. 42 pub fn sign<T: AsRef<[u8]>>(&self, msg: T) -> Result<Vec<u8>> { 43 match self { 44 Self::Signify(private_key) => Ok(private_key 45 .sign(msg.as_ref()) 46 .to_file_encoding("signed with git-signify via libsignify")), 47 Self::Minisign(private_key) => { 48 let signature_box = 49 minisign::sign(None, private_key, Cursor::new(msg.as_ref()), None, None) 50 .context("Failed to sign git object with minisign private key")?; 51 Ok(String::from(signature_box).into_bytes()) 52 } 53 Self::MlSignify(private_key) => { 54 let signature = ml_signify::sign(private_key, &ml_signify::hash(msg.as_ref())) 55 .context("Failed to sign git object with ml-signify private key")?; 56 57 let key_id = ml_signify::id_from_verifying_key(&private_key.verifying_key()); 58 59 Ok((key_id, &signature) 60 .ml_signify_encode::<codecs::Signature>(Some( 61 "signed with git-signify via ml-signify", 62 )) 63 .into_bytes()) 64 } 65 } 66 } 67 68 /// Return the algorithm of this [`PrivateKey`]. 69 pub const fn algorithm(&self) -> TreeSignatureAlgo { 70 match self { 71 Self::Signify(_) => TreeSignatureAlgo::Signify, 72 Self::Minisign(_) => TreeSignatureAlgo::Minisign, 73 Self::MlSignify(_) => TreeSignatureAlgo::MlSignify, 74 } 75 } 76} 77 78/// Public key used to verify signed git objects. 79pub enum PublicKey { 80 /// Public key originating from [`libsignify`]. 81 Signify(libsignify::PublicKey), 82 /// Public key originating from [`minisign`]. 83 Minisign(minisign::PublicKey), 84 /// Public key originating from [`ml_signify`]. 85 MlSignify(Box<ml_signify::VerifyingKey>), 86} 87 88impl PublicKey { 89 /// Compute the fingerprint of the given public key. 90 pub fn fingerprint(&self) -> Result<Oid> { 91 match self { 92 Self::Signify(public_key) => hash_bytes(public_key.key()) 93 .context("Failed to compute signify public key fingerprint"), 94 Self::Minisign(public_key) => hash_bytes(public_key.to_bytes()) 95 .context("Failed to compute minisign public key fingerprint"), 96 Self::MlSignify(public_key) => hash_bytes(public_key.encode()) 97 .context("Failed to compute ml-signify public key fingerprint"), 98 } 99 } 100} 101 102/// Enumeration of all possible versions of a [`TreeSignature`]. 103pub enum TreeSignatureVersion { 104 /// Version 0 tree signatures. 105 V0, 106 /// Version 1 tree signatures. 107 V1, 108 /// Version 2 tree signatures. 109 V2, 110 /// Version 3 tree signatures. 111 V3, 112} 113 114impl TreeSignatureVersion { 115 /// Parse a [`TreeSignatureVersion`] from a git [`Blob`]. 116 pub fn from_blob(blob: Blob<'_>) -> Result<Self> { 117 match blob.content() { 118 b"v1" => Ok(Self::V1), 119 b"v2" => Ok(Self::V2), 120 b"v3" => Ok(Self::V3), 121 blob => Err(anyhow!( 122 "Invalid tree signature version {:?}", 123 String::from_utf8_lossy(blob) 124 )), 125 } 126 } 127 128 /// Return the current version. 129 pub const fn current() -> Self { 130 TreeSignatureVersion::V3 131 } 132 133 /// Encode the version as a string. 134 pub const fn as_str(&self) -> &str { 135 match self { 136 Self::V0 => "v0", 137 Self::V1 => "v1", 138 Self::V2 => "v2", 139 Self::V3 => "v3", 140 } 141 } 142} 143 144/// Enumeration of all possible algorithms of a [`TreeSignature`]. 145pub enum TreeSignatureAlgo { 146 /// Signify key. 147 Signify, 148 /// Minisign key. 149 Minisign, 150 /// ML-Signify key. 151 MlSignify, 152} 153 154impl TreeSignatureAlgo { 155 /// Parse a [`TreeSignatureAlgo`] from a git [`Blob`]. 156 pub fn from_blob(blob: Blob<'_>) -> Result<Self> { 157 match blob.content() { 158 b"signify" => Ok(Self::Signify), 159 b"minisign" => Ok(Self::Minisign), 160 b"ml-signify" => Ok(Self::MlSignify), 161 blob => Err(anyhow!( 162 "Invalid tree signature algorithm {:?}", 163 String::from_utf8_lossy(blob) 164 )), 165 } 166 } 167 168 /// Encode the algorithm as a string. 169 pub const fn as_str(&self) -> &str { 170 match self { 171 Self::Signify => "signify", 172 Self::Minisign => "minisign", 173 Self::MlSignify => "ml-signify", 174 } 175 } 176} 177 178/// A signature stored in a git tree object. 179pub struct TreeSignature<'repo> { 180 /// Version of the tree signature. 181 pub version: TreeSignatureVersion, 182 /// Algorithm of the tree signature. 183 pub algorithm: TreeSignatureAlgo, 184 /// Pointer to the object that was signed. 185 pub object_pointer: Object<'repo>, 186 /// The signature over the git object. 187 pub signature: Blob<'repo>, 188} 189 190/// Parse a revision `rev`, and dispatch between `ok_` or `else_`, in case it is 191/// found (or not). 192pub fn revparse_single_ok_or_else<'repo, OK, ELSE, T>( 193 repo: &'repo Repository, 194 rev: &str, 195 ok_: OK, 196 else_: ELSE, 197) -> Result<T> 198where 199 OK: FnOnce(Object<'repo>) -> Result<T>, 200 ELSE: FnOnce() -> Result<T>, 201{ 202 match repo.revparse_single(rev) { 203 Ok(obj) => ok_(obj), 204 Err(e) if e.code() == ErrorCode::NotFound => else_(), 205 Err(e) => Err(e).context("Failed to look-up revision"), 206 } 207} 208 209impl<'repo> TreeSignature<'repo> { 210 /// Load a [`TreeSignature`] at the given `tree_rev` from the 211 /// provided git repository. The value of `tree_rev` is expected 212 /// to follow the refspec [`ALL_SIGNIFY_SIGNATURE_REFS`]. 213 #[inline] 214 pub fn load(repo: &'repo Repository, tree_rev: &str) -> Result<Option<Self>> { 215 revparse_single_ok_or_else( 216 repo, 217 tree_rev, 218 |obj| Self::load_oid(repo, obj.id()).map(Some), 219 || Ok(None), 220 ) 221 .context("Failed to look-up tree signature") 222 } 223 224 /// Like [`TreeSignature::load`], but uses a concrete revision pointing 225 /// to the tree signature. 226 pub fn load_oid(repo: &'repo Repository, oid: Oid) -> Result<Self> { 227 let object = repo 228 .find_object(oid, None) 229 .context("No git object found for the given revision")?; 230 231 match object.kind().context( 232 "Failed to determine kind of git object, while determining version of the signature", 233 )? { 234 ObjectType::Tree => Self::load_obj_v0(repo, object), 235 ObjectType::Commit => Self::load_obj_v1_or_greater(repo, object), 236 _ => anyhow::bail!( 237 "Invalid object kind provided, while loading tree signature with oid={oid}" 238 ), 239 } 240 } 241 242 /// Load a v0 [`TreeSignature`]. 243 fn load_obj_v0(repo: &'repo Repository, object: Object<'repo>) -> Result<Self> { 244 let tree = object.as_tree().with_context(|| { 245 format!( 246 "No tree signature found for object with oid={}", 247 object.id() 248 ) 249 })?; 250 251 let object_pointer = tree 252 .get_name("object") 253 .context("Failed to look-up signed object in the tree")? 254 .to_object(repo) 255 .context("The signed object could not be retrieved")?; 256 let signature = { 257 let signature = tree 258 .get_name("signature") 259 .context("Failed to look-up signature in the tree")? 260 .to_object(repo) 261 .context("The signature object could not be retrieved")?; 262 signature 263 .into_blob() 264 .map_err(|_| anyhow!("The signature object in oid={} is not a blob", object.id()))? 265 }; 266 267 Ok(Self { 268 signature, 269 object_pointer, 270 version: TreeSignatureVersion::V0, 271 algorithm: TreeSignatureAlgo::Signify, 272 }) 273 } 274 275 /// Load a v1 [`TreeSignature`]. 276 fn load_obj_v1_or_greater(repo: &'repo Repository, object: Object<'repo>) -> Result<Self> { 277 let commit = object 278 .as_commit() 279 .context("Failed to retrieve v1 git commit with signature")?; 280 let tree = commit 281 .tree() 282 .context("Failed to retrieve v1 git tree with signature")?; 283 284 let version = { 285 let version_obj = tree 286 .get_name("version") 287 .context("Failed to look-up tree signature version")? 288 .to_object(repo) 289 .context("The tree signature version could not be retrieved")?; 290 let version_blob = match version_obj.into_blob() { 291 Ok(blob) => blob, 292 Err(_) => return Err(anyhow!("The tree signature version object is not a blob")), 293 }; 294 TreeSignatureVersion::from_blob(version_blob)? 295 }; 296 let algorithm = { 297 let algorithm_obj = tree 298 .get_name("algorithm") 299 .context("Failed to look-up tree signature algorithm")? 300 .to_object(repo) 301 .context("The tree signature algorithm could not be retrieved")?; 302 let algorithm_blob = match algorithm_obj.into_blob() { 303 Ok(blob) => blob, 304 Err(_) => return Err(anyhow!("The tree signature algorithm object is not a blob")), 305 }; 306 TreeSignatureAlgo::from_blob(algorithm_blob)? 307 }; 308 309 let object_pointer = tree.get_name("object").map_or_else( 310 || match &version { 311 TreeSignatureVersion::V0 => { 312 anyhow::bail!("Attempted to parse v0 tree signature from commit object") 313 } 314 TreeSignatureVersion::V1 => Ok(commit 315 .parent(0) 316 .context( 317 "No signed `object` in the tree signature nor a parent commit \ 318 to be signed could be found", 319 )? 320 .into_object()), 321 TreeSignatureVersion::V2 | TreeSignatureVersion::V3 => { 322 anyhow::bail!("No signed `object` could be found in the tree signature"); 323 } 324 }, 325 |entry| { 326 entry.to_object(repo).with_context(|| { 327 format!( 328 "The signed object with oid={} could not be cast to a git object", 329 entry.id() 330 ) 331 }) 332 }, 333 )?; 334 335 let signature = { 336 let signature = tree 337 .get_name("signature") 338 .context("Failed to look-up signature in the tree")? 339 .to_object(repo) 340 .context("The signature object could not be retrieved")?; 341 signature 342 .into_blob() 343 .map_err(|_| anyhow!("The signature object in oid={} is not a blob", object.id()))? 344 }; 345 346 Ok(Self { 347 version, 348 algorithm, 349 signature, 350 object_pointer, 351 }) 352 } 353 354 /// Verify the authenticity of this [`TreeSignature`]. 355 pub fn verify(&self, public_key: &PublicKey) -> Result<()> { 356 self.check_compatibility(public_key) 357 .context("Incompatible public key provided")?; 358 359 match public_key { 360 PublicKey::Signify(public_key) => { 361 let signature = match &self.version { 362 TreeSignatureVersion::V0 => { 363 libsignify::Signature::from_bytes(self.signature.content()) 364 .map_err(Error::new) 365 .context("Failed to parse signify signature from git blob")? 366 } 367 TreeSignatureVersion::V1 368 | TreeSignatureVersion::V2 369 | TreeSignatureVersion::V3 => { 370 let signature_content = std::str::from_utf8(self.signature.content()) 371 .context("Found non-utf8 data in signify signature content")?; 372 373 let (signature, _) = libsignify::Signature::from_base64(signature_content) 374 .map_err(Error::new) 375 .context("Failed to parse signify signature from git blob")?; 376 377 signature 378 } 379 }; 380 381 let dereferenced_obj = self.dereference()?; 382 383 public_key 384 .verify(dereferenced_obj.as_bytes(), &signature) 385 .map_err(Error::new) 386 .context("Invalid signify signature") 387 } 388 PublicKey::Minisign(public_key) => { 389 let signature_box = match &self.version { 390 TreeSignatureVersion::V0 => { 391 anyhow::bail!("minisign public keys not supported in v0"); 392 } 393 TreeSignatureVersion::V1 394 | TreeSignatureVersion::V2 395 | TreeSignatureVersion::V3 => { 396 let signature_content = std::str::from_utf8(self.signature.content()) 397 .context("Found non-utf8 data in minisign signature content")?; 398 399 minisign::SignatureBox::from_string(signature_content) 400 .context("Failed to parse minisign signature from git blob")? 401 } 402 }; 403 404 let dereferenced_obj = self.dereference()?; 405 406 minisign::verify( 407 public_key, 408 &signature_box, 409 Cursor::new(dereferenced_obj.as_bytes()), 410 true, 411 false, 412 false, 413 ) 414 .context("Invalid minisign signature") 415 } 416 PublicKey::MlSignify(public_key) => { 417 let signature = match &self.version { 418 TreeSignatureVersion::V0 419 | TreeSignatureVersion::V1 420 | TreeSignatureVersion::V2 => { 421 anyhow::bail!("ml-signify public keys are only supported from v3 onwards"); 422 } 423 TreeSignatureVersion::V3 => { 424 let signature_content = std::str::from_utf8(self.signature.content()) 425 .context("Found non-utf8 data in ml-signify signature content")?; 426 427 let (_, signature) = signature_content 428 .ml_signify_decode::<codecs::Signature>() 429 .context("Failed to parse ml-signify signature from git blob")?; 430 431 signature 432 } 433 }; 434 435 let dereferenced_obj = self.dereference()?; 436 let message = ml_signify::hash(dereferenced_obj.as_bytes()); 437 438 anyhow::ensure!( 439 ml_signify::verify(public_key, &message, &signature), 440 "Invalid ml-signify signature" 441 ); 442 443 Ok(()) 444 } 445 } 446 } 447 448 /// Check the compatibility of the given public key with this 449 /// tree signature. 450 pub fn check_compatibility(&self, key: &PublicKey) -> Result<()> { 451 match (&self.version, &self.algorithm, key) { 452 (TreeSignatureVersion::V0, TreeSignatureAlgo::Signify, PublicKey::Signify(_)) 453 | (TreeSignatureVersion::V1, TreeSignatureAlgo::Signify, PublicKey::Signify(_)) 454 | (TreeSignatureVersion::V1, TreeSignatureAlgo::Minisign, PublicKey::Minisign(_)) 455 | (TreeSignatureVersion::V2, TreeSignatureAlgo::Signify, PublicKey::Signify(_)) 456 | (TreeSignatureVersion::V2, TreeSignatureAlgo::Minisign, PublicKey::Minisign(_)) 457 | (TreeSignatureVersion::V3, TreeSignatureAlgo::Signify, PublicKey::Signify(_)) 458 | (TreeSignatureVersion::V3, TreeSignatureAlgo::Minisign, PublicKey::Minisign(_)) 459 | (TreeSignatureVersion::V3, TreeSignatureAlgo::MlSignify, PublicKey::MlSignify(_)) => { 460 Ok(()) 461 } 462 _ => { 463 anyhow::bail!( 464 "Attempted to validate signature with a public key of an incompatible \ 465 type" 466 ); 467 } 468 } 469 } 470 471 /// Dereference the inner object pointer. 472 #[inline] 473 pub fn dereference(&self) -> Result<Oid> { 474 match &self.version { 475 TreeSignatureVersion::V0 => { 476 let blob = self 477 .object_pointer 478 .as_blob() 479 .context("The signed object is not a blob")?; 480 let oid_bytes = blob.content(); 481 Oid::from_bytes(oid_bytes).context("Failed to parse git object id from raw bytes") 482 } 483 TreeSignatureVersion::V1 | TreeSignatureVersion::V2 | TreeSignatureVersion::V3 => { 484 Ok(self.object_pointer.id()) 485 } 486 } 487 } 488} 489 490/// An error type. 491#[derive(Debug)] 492pub struct Error<E> { 493 inner: E, 494} 495 496impl<E: fmt::Display> fmt::Display for Error<E> { 497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 498 write!(f, "{}", self.inner) 499 } 500} 501 502impl<E: fmt::Display + fmt::Debug> error::Error for Error<E> {} 503 504impl<E> Error<E> { 505 /// Create a new [`Error`]. 506 pub fn new(inner: E) -> Self { 507 Self { inner } 508 } 509} 510 511/// Hash the provided bytearray and return the 512/// resulting checksum. 513#[inline] 514fn hash_bytes<T: AsRef<[u8]>>(bytes: T) -> Result<Oid> { 515 Oid::hash_object(ObjectType::Blob, bytes.as_ref()).context("Failed to hash bytes") 516} 517 518/// Determine the format of the given key data. 519fn determine_key_format(key_data: &str) -> Result<TreeSignatureAlgo> { 520 const UNTRUSTED_COMMENT: &str = "untrusted comment: "; 521 522 let Some(("", rest)) = key_data.split_once(UNTRUSTED_COMMENT) else { 523 anyhow::bail!("Unknown key format"); 524 }; 525 526 match rest { 527 s if s.starts_with("signify") => Ok(TreeSignatureAlgo::Signify), 528 s if s.starts_with("minisign") => Ok(TreeSignatureAlgo::Minisign), 529 s if s.starts_with("ml-signify") => Ok(TreeSignatureAlgo::MlSignify), 530 _ => Err(anyhow!("Unknown key format")), 531 } 532} 533 534/// Read all keys under the given `path` with `read`. 535fn read_key_entries<F, T>(ext: &str, path: PathBuf, mut read: F) -> Result<BTreeMap<PathBuf, T>> 536where 537 F: FnMut(&Path) -> Result<T>, 538{ 539 let mut keys = BTreeMap::new(); 540 541 for maybe_ent in fs::read_dir(path) 542 .with_context(|| format!("Failed to query entries in {ext} key directory"))? 543 { 544 let ent = 545 maybe_ent.with_context(|| format!("Failed to read entry in {ext} key directory"))?; 546 let path = ent.path(); 547 548 if matches!(path.extension().and_then(|p| p.to_str()), Some(e) if e == ext) { 549 let key = read(&path)?; 550 keys.insert(path, key); 551 } 552 } 553 554 Ok(keys) 555} 556 557/// Read public keys from the given path. If a directory is provided, 558/// keys are read from files whose extension is `.pub`. 559pub fn get_public_keys(path: PathBuf) -> Result<BTreeMap<PathBuf, PublicKey>> { 560 let meta = fs::metadata(&path).context("Failed to query public key path metadata")?; 561 562 if meta.is_dir() { 563 read_key_entries("pub", path, get_public_key) 564 } else { 565 get_public_key(&path).map(|key| { 566 let mut map = BTreeMap::new(); 567 map.insert(path, key); 568 map 569 }) 570 } 571} 572 573/// Read a public key from the given path. 574fn get_public_key(path: &Path) -> Result<PublicKey> { 575 let key_data = std::fs::read_to_string(path).context("Failed to read public key")?; 576 577 Ok(match determine_key_format(&key_data)? { 578 TreeSignatureAlgo::Signify => { 579 let (public_key, _) = libsignify::PublicKey::from_base64(&key_data[..]) 580 .map_err(Error::new) 581 .context("Failed to decode signify public key")?; 582 583 PublicKey::Signify(public_key) 584 } 585 TreeSignatureAlgo::Minisign => { 586 let public_key = minisign::PublicKeyBox::from_string(&key_data[..]) 587 .context("Failed to read minisign public key")?; 588 589 PublicKey::Minisign( 590 public_key 591 .into_public_key() 592 .context("Failed to decode minisign public key")?, 593 ) 594 } 595 TreeSignatureAlgo::MlSignify => { 596 let (_, public_key) = key_data 597 .ml_signify_decode::<codecs::VerifyingKey>() 598 .context("Failed to decode ml-signify verifying key")?; 599 600 PublicKey::MlSignify(Box::new(public_key)) 601 } 602 }) 603} 604 605/// Read secret keys from the given path. If a directory is provided, 606/// keys are read from files whose extension is `.sec`. 607pub fn get_secret_keys(path: PathBuf) -> Result<BTreeMap<PathBuf, PrivateKey>> { 608 let meta = fs::metadata(&path).context("Failed to query secret key path metadata")?; 609 610 if meta.is_dir() { 611 read_key_entries("sec", path, get_secret_key) 612 } else { 613 get_secret_key(&path).map(|key| { 614 let mut map = BTreeMap::new(); 615 map.insert(path, key); 616 map 617 }) 618 } 619} 620 621/// Read a secret key from the given path. 622fn get_secret_key(path: &Path) -> Result<PrivateKey> { 623 let key_data = std::fs::read_to_string(path) 624 .map(Zeroizing::new) 625 .context("Failed to read secret key")?; 626 627 Ok(match determine_key_format(&key_data)? { 628 TreeSignatureAlgo::Signify => { 629 let (mut secret_key, _) = libsignify::PrivateKey::from_base64(&key_data[..]) 630 .map_err(Error::new) 631 .context("Failed to decode secret key")?; 632 633 if secret_key.is_encrypted() { 634 let passphrase = prompt_key_passphrase(path).map(Zeroizing::new)?; 635 636 secret_key 637 .decrypt_with_password(&passphrase) 638 .map_err(Error::new) 639 .context("Failed to decrypt secret key")?; 640 } 641 642 PrivateKey::Signify(secret_key) 643 } 644 TreeSignatureAlgo::Minisign => { 645 let private_key = minisign::SecretKeyBox::from_string(&key_data[..]) 646 .context("Failed to read minisign secret key")?; 647 648 let passphrase = prompt_key_passphrase(path)?; 649 650 PrivateKey::Minisign( 651 private_key 652 .into_secret_key(Some(passphrase)) 653 .context("Failed to decode minisign private key")?, 654 ) 655 } 656 TreeSignatureAlgo::MlSignify => { 657 let sealed_key = key_data 658 .ml_signify_decode::<codecs::SigningKey>() 659 .context("Failed to decode ml-signify sealed secret key")?; 660 661 let passphrase = prompt_key_passphrase(path)?; 662 663 let private_key = 664 ml_signify::seal::unseal_signing_key(&sealed_key, passphrase.as_bytes()) 665 .context("Failed to unseal ml-signify secret key")?; 666 667 PrivateKey::MlSignify(Box::new(private_key.signing_key().clone())) 668 } 669 }) 670} 671 672fn prompt_key_passphrase(path: &Path) -> Result<String> { 673 rpassword::prompt_password(format!("{} passphrase: ", path.display())) 674 .context("Failed to read secret key passphrase") 675} 676 677/// Try to find and open a git repository. 678pub fn open_repository() -> Result<Repository> { 679 Repository::open_ext( 680 ".", 681 RepositoryOpenFlags::empty(), 682 &[] as &[&std::ffi::OsStr], 683 ) 684 .context("Failed to open git repository") 685} 686 687/// Craft a git reference to an object signed by a key with the given 688/// fingerprint. 689pub fn craft_signature_reference(key_fingerprint: Oid, signed_object: Oid) -> String { 690 format!("refs/signify/signatures/{key_fingerprint}/{signed_object}") 691} 692 693/// Git refspec describing all signify references. 694pub const ALL_SIGNIFY_REFS: &str = "refs/signify/*"; 695 696/// Git refspec describing all signify signature references. 697pub const ALL_SIGNIFY_SIGNATURE_REFS: &str = "refs/signify/signatures/*"; 698 699/// Git refspec prefix describing all signify signature references. 700pub const ALL_SIGNIFY_SIGNATURE_REFS_PREFIX: &str = "refs/signify/signatures/";