Skip to main content

boundgraph_check/
sign.rs

1//! Signing a verdict, and verifying one without asking anybody.
2//!
3//! # Why a verdict has to be signed at all
4//!
5//! An unsigned verdict is a row in a log file. Anyone who can write the log can
6//! write "the tests passed", and nobody reading it later can tell the
7//! difference between a checker that ran and a line someone typed. That is the
8//! whole gap between logging and evidence, and Law III sits on the far side of
9//! it: a state change commits on [`Evidence`], so evidence has to be worth
10//! something.
11//!
12//! ed25519 over the canonical CBOR encoding closes it. The signature covers the
13//! verdict, the checker's identity and version, the digests of exactly what was
14//! checked, the key identity, and how the verdict was reached — everything
15//! except the signature itself, which is what
16//! [`Evidence::signing_payload`](boundgraph_core::Evidence::signing_payload) returns.
17//! Change any of it and the signature stops verifying.
18//!
19//! # Deterministic signatures, deliberately
20//!
21//! ed25519 derives its per-signature nonce from the private key and the message
22//! (RFC 8032 §5.1.6) rather than sampling one. Signing the same evidence twice
23//! therefore produces the same 64 bytes. That is load-bearing here rather than
24//! incidental: US-0801 requires a checker to be a pure function of its inputs,
25//! and replay (US-1006) re-runs a recorded check and compares. A randomised
26//! signature scheme would make every replayed run differ in a field that says
27//! nothing about whether the check agreed.
28//!
29//! # Verification is a free function
30//!
31//! [`verify`] takes evidence and a public key and returns an answer. No
32//! runtime, no registry, no async, no I/O. A compliance reviewer with a ledger
33//! file and a published key can check every verdict in it without running BoundGraph
34//! at all — which is the only version of "auditable" that means anything.
35//!
36//! # There is no path to unsigned evidence
37//!
38//! [`sign_evidence`] is the only constructor in this crate, and it returns
39//! `Result`. There is no `unsigned()`, no feature flag, and no fallback that
40//! degrades to an empty signature when a key is unavailable. A signing failure
41//! propagates and the step does not commit. That asymmetry is deliberate: a
42//! blocked commit is an outage, an unsigned commit is a lie in the audit trail,
43//! and only one of those can be fixed after the fact.
44//!
45//! # Key management
46//!
47//! Which keys exist, and who holds them, is unresolved — `ARCHITECTURE.md` §17
48//! lists it as the one open question blocking M3. `docs/evidence-keys.md`
49//! records the three candidate strategies and what each costs. This module is
50//! written so that the decision can be made later without changing any caller:
51//!
52//! - [`Sign`] is a trait, so an external KMS or an HSM is an implementation of
53//!   it rather than a rewrite. [`LocalKey`] is the in-process one, which is
54//!   what M3 runs on.
55//! - The key *identity* travels in the evidence, not the key material, so a
56//!   verifier looks the key up rather than trusting what it was handed.
57//! - [`KeyRing`] is the smallest possible resolver of identity to public key.
58//!   A KMS-backed one satisfies the same call.
59
60use std::collections::BTreeMap;
61use std::fmt;
62
63use boundgraph_core::{
64    CanonicalError, CheckerId, Digest, Evidence, EvidenceKind, Signature, SigningKeyId, Verdict,
65};
66use ed25519_dalek::{Signer as _, Verifier as _};
67
68/// How many bytes an ed25519 public key is.
69pub const PUBLIC_KEY_LEN: usize = 32;
70
71/// How many bytes a private seed is.
72pub const SEED_LEN: usize = 32;
73
74/// Signing failed, so nothing commits.
75///
76/// Every variant is terminal for the check that produced it. See the module
77/// documentation on why there is no degraded path.
78#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79#[non_exhaustive]
80pub enum SignError {
81    /// The evidence could not be canonically encoded, so there is nothing to
82    /// sign over.
83    #[error("the evidence could not be encoded for signing: {0}")]
84    Encode(String),
85
86    /// The signer could not produce a signature — an unreachable KMS, a
87    /// revoked key, a hardware token that is not present.
88    #[error("the signer {key} is unavailable: {detail}")]
89    Unavailable {
90        /// Which key could not be used.
91        key: SigningKeyId,
92        /// Why.
93        detail: String,
94    },
95}
96
97impl From<CanonicalError> for SignError {
98    fn from(e: CanonicalError) -> Self {
99        Self::Encode(e.to_string())
100    }
101}
102
103/// Verification failed, so the evidence proves nothing.
104#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
105#[non_exhaustive]
106pub enum VerifyError {
107    /// The signature does not match the evidence under the given key.
108    ///
109    /// Says nothing about *which* field was altered, and cannot: a signature is
110    /// over the whole pre-image. What it does say is that this evidence is not
111    /// what the key signed.
112    #[error("the signature over evidence from {checker} does not verify under {key}")]
113    Invalid {
114        /// The checker the evidence claims to come from.
115        checker: CheckerId,
116        /// The key it claims to be signed by.
117        key: SigningKeyId,
118    },
119
120    /// The evidence names a key the verifier does not have.
121    ///
122    /// Not a failure of the evidence. An auditor missing a public key has an
123    /// incomplete key ring, and treating that as tampering would be wrong —
124    /// which is why it is its own variant.
125    #[error("no public key is known for {0}")]
126    UnknownKey(SigningKeyId),
127
128    /// The public key bytes are not a valid ed25519 point.
129    #[error("the public key for {key} is malformed: {detail}")]
130    MalformedKey {
131        /// Which key.
132        key: SigningKeyId,
133        /// Why it could not be used.
134        detail: String,
135    },
136
137    /// The evidence could not be canonically encoded, so the pre-image the
138    /// signature covers cannot be reconstructed.
139    #[error("the evidence could not be encoded for verification: {0}")]
140    Encode(String),
141}
142
143/// An ed25519 public key, as bytes.
144///
145/// Deliberately a plain 32-byte value rather than a re-exported curve type: a
146/// key ring, a ledger, and a config file all want to carry one, and none of
147/// them should have to name this crate's ed25519 implementation to do it.
148/// Point validity is checked at [`verify`] time, where the error can be
149/// reported, rather than at construction where it would only panic.
150#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
151pub struct PublicKey([u8; PUBLIC_KEY_LEN]);
152
153impl PublicKey {
154    /// Wrap raw public key bytes.
155    pub const fn from_bytes(bytes: [u8; PUBLIC_KEY_LEN]) -> Self {
156        Self(bytes)
157    }
158
159    /// The raw bytes.
160    pub const fn as_bytes(&self) -> &[u8; PUBLIC_KEY_LEN] {
161        &self.0
162    }
163
164    /// The key as lowercase hex, which is how it appears in configuration.
165    pub fn to_hex(&self) -> String {
166        use fmt::Write as _;
167        self.0.iter().fold(String::new(), |mut s, b| {
168            let _ = write!(s, "{b:02x}");
169            s
170        })
171    }
172
173    fn to_dalek(self, id: &SigningKeyId) -> Result<ed25519_dalek::VerifyingKey, VerifyError> {
174        ed25519_dalek::VerifyingKey::from_bytes(&self.0).map_err(|e| VerifyError::MalformedKey {
175            key: id.clone(),
176            detail: e.to_string(),
177        })
178    }
179}
180
181impl fmt::Debug for PublicKey {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "PublicKey({})", self.to_hex())
184    }
185}
186
187/// Whatever can produce a signature for a key it holds.
188///
189/// The seam the open key-management question lives behind. An in-process key,
190/// an agent socket, a cloud KMS, and a hardware token differ in where the
191/// private bytes are and how a signature is requested; none of that is visible
192/// to a checker, which asks for a signature and gets one or does not commit.
193pub trait Sign: Send + Sync + fmt::Debug {
194    /// Which key this signs with.
195    ///
196    /// Recorded in every piece of evidence, so a verifier knows what to check
197    /// against without being handed the key material.
198    fn key_id(&self) -> SigningKeyId;
199
200    /// Sign a pre-image.
201    ///
202    /// # Errors
203    ///
204    /// [`SignError::Unavailable`] if the key could not be used. There is no
205    /// success value that means "unsigned".
206    fn sign(&self, payload: &[u8]) -> Result<Signature, SignError>;
207}
208
209/// An ed25519 key held in this process.
210///
211/// What M3 runs on, and the reference implementation of [`Sign`]. The private
212/// bytes are zeroized on drop by `ed25519-dalek`, and neither [`Debug`] nor any
213/// accessor exposes them — a key that has been printed into a log is a key that
214/// has been published.
215///
216/// # Example
217///
218/// ```
219/// use boundgraph_check::{LocalKey, Sign, sign_evidence, verify};
220/// use boundgraph_core::{CheckerId, Digest, EvidenceKind, Verdict};
221///
222/// let key = LocalKey::from_seed("host-key-1", [7u8; 32]);
223///
224/// let evidence = sign_evidence(
225///     Verdict::Pass,
226///     CheckerId::new("json-schema", 1),
227///     [Digest::of(b"the payload that was checked")],
228///     EvidenceKind::Deterministic,
229///     &key,
230/// )?;
231///
232/// // Anyone holding the public key can check it, with no runtime at all.
233/// verify(&evidence, &key.public_key())?;
234/// # Ok::<(), Box<dyn std::error::Error>>(())
235/// ```
236pub struct LocalKey {
237    id: SigningKeyId,
238    key: ed25519_dalek::SigningKey,
239}
240
241impl LocalKey {
242    /// Build a key from a 32-byte seed.
243    ///
244    /// Deterministic: the same seed always gives the same key. That is what
245    /// makes a cassette test (US-1008) able to assert on exact signature bytes,
246    /// and it is why this takes a seed rather than sampling one.
247    ///
248    /// The seed *is* the private key. Treat it as such.
249    pub fn from_seed(id: impl Into<String>, seed: [u8; SEED_LEN]) -> Self {
250        Self {
251            id: SigningKeyId(id.into()),
252            key: ed25519_dalek::SigningKey::from_bytes(&seed),
253        }
254    }
255
256    /// Build a key from freshly sampled entropy.
257    ///
258    /// For a per-run key, which is one of the strategies
259    /// `docs/evidence-keys.md` weighs. Not deterministic, and so not for use in
260    /// a test that asserts on signature bytes.
261    pub fn generate(id: impl Into<String>) -> Self {
262        use rand::RngCore as _;
263        let mut seed = [0u8; SEED_LEN];
264        rand::rng().fill_bytes(&mut seed);
265        Self::from_seed(id, seed)
266    }
267
268    /// The public half, to be published so verdicts can be checked.
269    pub fn public_key(&self) -> PublicKey {
270        PublicKey(self.key.verifying_key().to_bytes())
271    }
272}
273
274impl fmt::Debug for LocalKey {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        // The public half only. A private key in a debug log is a leaked key.
277        f.debug_struct("LocalKey")
278            .field("id", &self.id)
279            .field("public_key", &self.public_key())
280            .finish()
281    }
282}
283
284impl Sign for LocalKey {
285    fn key_id(&self) -> SigningKeyId {
286        self.id.clone()
287    }
288
289    fn sign(&self, payload: &[u8]) -> Result<Signature, SignError> {
290        Ok(Signature::from_bytes(self.key.sign(payload).to_bytes()))
291    }
292}
293
294/// Build evidence and sign it, in that order, as one operation.
295///
296/// The only way to make an [`Evidence`] in this crate. Splitting it into
297/// "construct" and "sign later" would create a window in which an unsigned
298/// value exists and could be stored, and there is no reason to want one.
299///
300/// Inputs are sorted and deduplicated by [`Evidence::new`] *before* the
301/// pre-image is computed, so a checker that happened to list the same digests
302/// in a different order produces byte-identical evidence.
303///
304/// # Errors
305///
306/// [`SignError::Encode`] if the evidence cannot be canonically encoded, or
307/// [`SignError::Unavailable`] if the signer refused. Either way the caller has
308/// no evidence, and so cannot commit.
309pub fn sign_evidence(
310    verdict: Verdict,
311    checker: CheckerId,
312    inputs: impl IntoIterator<Item = Digest>,
313    kind: EvidenceKind,
314    signer: &dyn Sign,
315) -> Result<Evidence, SignError> {
316    // A placeholder purely so the pre-image can be computed from the finished
317    // value. It is overwritten below and never leaves this function: every exit
318    // path from here either carries a real signature or is an error.
319    let mut evidence = Evidence::new(
320        verdict,
321        checker,
322        inputs,
323        Signature::from_bytes([0u8; 64]),
324        signer.key_id(),
325        kind,
326    );
327
328    let payload = evidence.signing_payload()?;
329    evidence.signature = signer.sign(&payload)?;
330    Ok(evidence)
331}
332
333/// Check evidence against a public key.
334///
335/// Standalone by design: no runtime, no registry, no async, no I/O. Given a
336/// ledger file and a published key, this is the whole of what an external
337/// auditor needs.
338///
339/// # Errors
340///
341/// [`VerifyError::Invalid`] if the signature does not match — which is what a
342/// tampered value produces, whichever field was altered.
343/// [`VerifyError::MalformedKey`] if the key is not a valid ed25519 point, and
344/// [`VerifyError::Encode`] if the pre-image could not be rebuilt.
345pub fn verify(evidence: &Evidence, key: &PublicKey) -> Result<(), VerifyError> {
346    let verifying = key.to_dalek(&evidence.key)?;
347
348    let payload = evidence
349        .signing_payload()
350        .map_err(|e| VerifyError::Encode(e.to_string()))?;
351
352    let signature = ed25519_dalek::Signature::from_bytes(evidence.signature.as_bytes());
353
354    // `verify` on `VerifyingKey` is already the strict, cofactorless check that
355    // rejects small-order keys and non-canonical encodings. The malleable
356    // variant is not used, so two distinct signatures can never both verify
357    // over one piece of evidence.
358    verifying
359        .verify(&payload, &signature)
360        .map_err(|_| VerifyError::Invalid {
361            checker: evidence.checker.clone(),
362            key: evidence.key.clone(),
363        })
364}
365
366/// Public keys, by the identity the evidence names.
367///
368/// The smallest thing that resolves a [`SigningKeyId`] to a [`PublicKey`], and
369/// deliberately no more: whether keys arrive from a config file, a KMS, or a
370/// transparency log is a deployment question, and this type is what all three
371/// end up producing.
372#[derive(Debug, Clone, Default, PartialEq, Eq)]
373pub struct KeyRing {
374    keys: BTreeMap<SigningKeyId, PublicKey>,
375}
376
377impl KeyRing {
378    /// An empty ring, which trusts nothing.
379    pub fn new() -> Self {
380        Self::default()
381    }
382
383    /// Trust `key` under the identity `id`.
384    #[must_use]
385    pub fn with(mut self, id: impl Into<String>, key: PublicKey) -> Self {
386        self.keys.insert(SigningKeyId(id.into()), key);
387        self
388    }
389
390    /// Trust the public half of a local key under its own identity.
391    #[must_use]
392    pub fn trusting(self, key: &LocalKey) -> Self {
393        let id = key.key_id();
394        self.with(id.0, key.public_key())
395    }
396
397    /// The public key for an identity, if the ring holds one.
398    pub fn get(&self, id: &SigningKeyId) -> Option<&PublicKey> {
399        self.keys.get(id)
400    }
401
402    /// How many keys are trusted.
403    pub fn len(&self) -> usize {
404        self.keys.len()
405    }
406
407    /// Whether the ring trusts nothing.
408    pub fn is_empty(&self) -> bool {
409        self.keys.is_empty()
410    }
411
412    /// Check evidence against whichever key it names.
413    ///
414    /// # Errors
415    ///
416    /// [`VerifyError::UnknownKey`] if the ring does not hold the named key —
417    /// distinct from [`VerifyError::Invalid`], because an incomplete key ring
418    /// is the auditor's problem and a bad signature is the evidence's.
419    pub fn verify(&self, evidence: &Evidence) -> Result<(), VerifyError> {
420        let key = self
421            .get(&evidence.key)
422            .ok_or_else(|| VerifyError::UnknownKey(evidence.key.clone()))?;
423        verify(evidence, key)
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    fn key() -> LocalKey {
432        LocalKey::from_seed("host-key-1", [7u8; SEED_LEN])
433    }
434
435    fn evidence(signer: &dyn Sign) -> Evidence {
436        sign_evidence(
437            Verdict::Pass,
438            CheckerId::new("json-schema", 1),
439            [Digest::of(b"payload"), Digest::of(b"schema")],
440            EvidenceKind::Deterministic,
441            signer,
442        )
443        .expect("signing should succeed")
444    }
445
446    // ── signing ─────────────────────────────────────────────────────────────
447
448    /// The criterion: evidence is signed over its canonical encoding, and the
449    /// result verifies.
450    #[test]
451    fn signed_evidence_verifies_under_the_signing_key() {
452        let key = key();
453        assert!(verify(&evidence(&key), &key.public_key()).is_ok());
454    }
455
456    #[test]
457    fn the_signing_key_identity_is_recorded_in_the_evidence() {
458        let key = key();
459        assert_eq!(evidence(&key).key, SigningKeyId("host-key-1".into()));
460    }
461
462    /// A verifier that only had the key material would have to trust whatever
463    /// it was handed. It looks the key up by the recorded identity instead.
464    #[test]
465    fn a_key_ring_resolves_the_recorded_identity() {
466        let key = key();
467        let ring = KeyRing::new().trusting(&key);
468
469        assert_eq!(ring.len(), 1);
470        assert!(ring.verify(&evidence(&key)).is_ok());
471    }
472
473    #[test]
474    fn a_key_the_ring_does_not_hold_is_not_a_tampering_report() {
475        let key = key();
476        let err = KeyRing::new().verify(&evidence(&key)).unwrap_err();
477
478        assert_eq!(
479            err,
480            VerifyError::UnknownKey(SigningKeyId("host-key-1".into()))
481        );
482    }
483
484    /// ed25519 derives its nonce from the key and the message, so the same
485    /// evidence signs to the same bytes. Replay compares evidence.
486    #[test]
487    fn signing_the_same_evidence_twice_gives_the_same_bytes() {
488        let key = key();
489        let first = evidence(&key);
490        for _ in 0..8 {
491            assert_eq!(evidence(&key), first);
492        }
493    }
494
495    #[test]
496    fn input_order_does_not_change_the_signature() {
497        let key = key();
498        let forwards = sign_evidence(
499            Verdict::Pass,
500            CheckerId::new("c", 1),
501            [Digest::of(b"a"), Digest::of(b"b")],
502            EvidenceKind::Deterministic,
503            &key,
504        )
505        .unwrap();
506        let backwards = sign_evidence(
507            Verdict::Pass,
508            CheckerId::new("c", 1),
509            [Digest::of(b"b"), Digest::of(b"a")],
510            EvidenceKind::Deterministic,
511            &key,
512        )
513        .unwrap();
514
515        assert_eq!(forwards, backwards);
516    }
517
518    /// The placeholder used to compute the pre-image must never survive.
519    #[test]
520    fn the_placeholder_signature_never_escapes() {
521        let key = key();
522        assert_ne!(evidence(&key).signature, Signature::from_bytes([0u8; 64]));
523    }
524
525    // ── tampering ───────────────────────────────────────────────────────────
526
527    /// The criterion: every field the signature covers, checked one at a time.
528    #[test]
529    fn a_tampered_evidence_value_fails_verification() {
530        let key = key();
531        let public = key.public_key();
532        let good = evidence(&key);
533
534        let mut flipped_verdict = good.clone();
535        flipped_verdict.verdict = Verdict::Fail("actually it failed".into());
536
537        let mut flipped_checker = good.clone();
538        flipped_checker.checker = CheckerId::new("json-schema", 2);
539
540        let mut flipped_inputs = good.clone();
541        flipped_inputs.inputs = vec![Digest::of(b"a different payload")];
542
543        let mut flipped_key_id = good.clone();
544        flipped_key_id.key = SigningKeyId("some-other-key".into());
545
546        let mut flipped_kind = good.clone();
547        flipped_kind.kind = EvidenceKind::Quorum { n: 3, k: 2 };
548
549        let mut flipped_signature = good.clone();
550        flipped_signature.signature = Signature::from_bytes([9u8; 64]);
551
552        for (what, tampered) in [
553            ("verdict", flipped_verdict),
554            ("checker", flipped_checker),
555            ("inputs", flipped_inputs),
556            ("key identity", flipped_key_id),
557            ("kind", flipped_kind),
558            ("signature", flipped_signature),
559        ] {
560            assert!(
561                verify(&tampered, &public).is_err(),
562                "altering the {what} must break verification"
563            );
564        }
565    }
566
567    /// The most valuable forgery there is: taking a passing verdict and
568    /// pointing it at a patch that never passed.
569    #[test]
570    fn a_pass_cannot_be_moved_onto_another_artefact() {
571        let key = key();
572        let mut harvested = evidence(&key);
573        harvested.inputs = vec![Digest::of(b"a failing patch")];
574
575        assert!(verify(&harvested, &key.public_key()).is_err());
576    }
577
578    #[test]
579    fn evidence_does_not_verify_under_the_wrong_key() {
580        let key = key();
581        let other = LocalKey::from_seed("host-key-2", [9u8; SEED_LEN]);
582
583        let err = verify(&evidence(&key), &other.public_key()).unwrap_err();
584        assert!(matches!(err, VerifyError::Invalid { .. }));
585    }
586
587    #[test]
588    fn an_all_zero_signature_does_not_verify() {
589        let key = key();
590        let mut unsigned = evidence(&key);
591        unsigned.signature = Signature::from_bytes([0u8; 64]);
592
593        assert!(verify(&unsigned, &key.public_key()).is_err());
594    }
595
596    #[test]
597    fn a_malformed_public_key_is_its_own_error() {
598        let key = key();
599        // `y = 2` does not decompress: no `x` on the curve has it.
600        let mut bytes = [0u8; PUBLIC_KEY_LEN];
601        bytes[0] = 2;
602        let broken = PublicKey::from_bytes(bytes);
603
604        let err = verify(&evidence(&key), &broken).unwrap_err();
605        assert!(matches!(err, VerifyError::MalformedKey { .. }));
606    }
607
608    // ── signing failure ─────────────────────────────────────────────────────
609
610    /// The criterion: a signer that cannot sign yields no evidence at all,
611    /// rather than evidence with an empty signature.
612    #[test]
613    fn a_signing_failure_produces_no_evidence() {
614        #[derive(Debug)]
615        struct Broken;
616
617        impl Sign for Broken {
618            fn key_id(&self) -> SigningKeyId {
619                SigningKeyId("kms-key-1".into())
620            }
621            fn sign(&self, _payload: &[u8]) -> Result<Signature, SignError> {
622                Err(SignError::Unavailable {
623                    key: self.key_id(),
624                    detail: "the KMS did not answer".into(),
625                })
626            }
627        }
628
629        let err = sign_evidence(
630            Verdict::Pass,
631            CheckerId::new("json-schema", 1),
632            [Digest::of(b"payload")],
633            EvidenceKind::Deterministic,
634            &Broken,
635        )
636        .unwrap_err();
637
638        assert!(matches!(err, SignError::Unavailable { .. }));
639    }
640
641    // ── keys ────────────────────────────────────────────────────────────────
642
643    #[test]
644    fn the_same_seed_always_gives_the_same_key() {
645        let a = LocalKey::from_seed("k", [3u8; SEED_LEN]);
646        let b = LocalKey::from_seed("k", [3u8; SEED_LEN]);
647        assert_eq!(a.public_key(), b.public_key());
648    }
649
650    #[test]
651    fn generated_keys_differ() {
652        assert_ne!(
653            LocalKey::generate("k").public_key(),
654            LocalKey::generate("k").public_key()
655        );
656    }
657
658    /// A private key that has been printed is a private key that has been
659    /// published.
660    #[test]
661    fn debug_output_does_not_contain_the_private_seed() {
662        let seed = [0xab; SEED_LEN];
663        let rendered = format!("{:?}", LocalKey::from_seed("k", seed));
664
665        let seed_hex = PublicKey::from_bytes(seed).to_hex();
666        assert!(!rendered.contains(&seed_hex), "the seed leaked: {rendered}");
667        assert!(rendered.contains("host") || rendered.contains('k'));
668    }
669
670    #[test]
671    fn a_public_key_renders_as_hex() {
672        let key = PublicKey::from_bytes([0u8; PUBLIC_KEY_LEN]);
673        assert_eq!(key.to_hex().len(), PUBLIC_KEY_LEN * 2);
674        assert!(format!("{key:?}").contains(&key.to_hex()));
675    }
676
677    #[test]
678    fn an_empty_ring_trusts_nothing() {
679        let ring = KeyRing::new();
680        assert!(ring.is_empty());
681        assert!(ring.get(&SigningKeyId("anything".into())).is_none());
682    }
683}