Harness the power of signify(1) to sign arbitrary git objects
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/";