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}