dig_keystore/signer.rs
1//! `SignerHandle` — the unlocked, signing-capable handle returned by `Keystore::unlock`.
2//!
3//! A `SignerHandle<K>` owns a `Zeroizing<Vec<u8>>` copy of the decrypted secret
4//! and the derived public key. It exposes `sign` and `public_key`; raw secret
5//! bytes can never be extracted. Drop zeroizes the secret.
6
7use std::marker::PhantomData;
8
9use zeroize::Zeroizing;
10
11use crate::error::Result;
12use crate::scheme::KeyScheme;
13
14/// The unlocked handle. Drop wipes the secret.
15///
16/// Cloning a `SignerHandle` clones the underlying zeroizing buffer — both
17/// copies are independently wiped on drop. This is expensive for high-frequency
18/// signing; prefer sharing an `Arc<SignerHandle<K>>` for that case.
19pub struct SignerHandle<K: KeyScheme> {
20 secret: Zeroizing<Vec<u8>>,
21 public: K::PublicKey,
22 _marker: PhantomData<fn() -> K>,
23}
24
25impl<K: KeyScheme> SignerHandle<K> {
26 pub(crate) fn from_parts(secret: Zeroizing<Vec<u8>>, public: K::PublicKey) -> Self {
27 Self {
28 secret,
29 public,
30 _marker: PhantomData,
31 }
32 }
33
34 /// Borrow the derived public key. Cheap (precomputed at unlock time).
35 pub fn public_key(&self) -> &K::PublicKey {
36 &self.public
37 }
38
39 /// Sign a byte message.
40 pub fn sign(&self, msg: &[u8]) -> K::Signature {
41 // K::sign only errors when the secret length is wrong; we control that.
42 K::sign(&self.secret, msg).expect("signer handle secret length is guaranteed valid")
43 }
44
45 /// Attempt to sign, surfacing any scheme-level errors instead of panicking.
46 pub fn try_sign(&self, msg: &[u8]) -> Result<K::Signature> {
47 K::sign(&self.secret, msg)
48 }
49}
50
51impl<K: KeyScheme> Clone for SignerHandle<K> {
52 fn clone(&self) -> Self {
53 Self {
54 secret: self.secret.clone(),
55 public: self.public.clone(),
56 _marker: PhantomData,
57 }
58 }
59}
60
61impl<K: KeyScheme> std::fmt::Debug for SignerHandle<K> {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("SignerHandle")
64 .field("scheme", &K::NAME)
65 .field("public", &self.public)
66 .field(
67 "secret",
68 &format_args!("<{} bytes zeroized>", self.secret.len()),
69 )
70 .finish()
71 }
72}
73
74// Explicitly NOT implementing AsRef<[u8]>, Deref, or into_raw() on SignerHandle.
75// The secret never leaves the handle by design.
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::scheme::BlsSigning;
81
82 /// **Proves:** the `Debug` impl of `SignerHandle` does not print the raw
83 /// secret bytes. We construct a handle with secret `0xAA` × 32 and
84 /// assert the formatted output contains the placeholder but not `"AA"`.
85 ///
86 /// **Why it matters:** `SignerHandle` is routinely stored inside the
87 /// validator's `Node` struct which gets `tracing::info!(?self, ...)`ed
88 /// at startup. If the Debug impl leaked the secret, validator keys
89 /// would land in log files on every restart. The test pins the no-leak
90 /// property.
91 ///
92 /// **Catches:** accidentally deriving `Debug` on `SignerHandle` (which
93 /// would print the inner `Zeroizing<Vec<u8>>` content), or a future
94 /// `#[derive(Debug)]` addition that bypasses the custom impl.
95 #[test]
96 fn debug_does_not_leak_secret() {
97 let secret = Zeroizing::new(vec![0xAAu8; 32]);
98 let public = BlsSigning::public_key(&secret).unwrap();
99 let handle: SignerHandle<BlsSigning> = SignerHandle::from_parts(secret, public);
100 let s = format!("{:?}", handle);
101 assert!(s.contains("<32 bytes zeroized>"));
102 assert!(!s.contains("AA"));
103 }
104
105 /// **Proves:** the full in-memory signing path works — construct a
106 /// handle, sign, verify with the public key.
107 ///
108 /// **Why it matters:** Exercises `SignerHandle::sign` without going
109 /// through the encrypted backend. If this ever regresses, `Keystore::unlock`
110 /// would return a handle that produces wrong signatures (catastrophic).
111 ///
112 /// **Catches:** a bug in `sign` (e.g. forwarding the wrong secret field,
113 /// signing the wrong bytes, feeding the public key as the secret key).
114 #[test]
115 fn sign_works() {
116 let secret = Zeroizing::new(vec![0x11u8; 32]);
117 let public = BlsSigning::public_key(&secret).unwrap();
118 let handle: SignerHandle<BlsSigning> = SignerHandle::from_parts(secret, public);
119 let sig = handle.sign(b"message");
120 assert!(chia_bls::verify(&sig, &public, b"message"));
121 }
122
123 /// **Proves:** cloning a `SignerHandle` yields an independent copy that
124 /// produces the exact same signature as the original.
125 ///
126 /// **Why it matters:** Validators sometimes clone the handle into a
127 /// per-duty context (so a panic in one duty's signing doesn't poison
128 /// the other's). Both copies must produce identical signatures, which
129 /// they will iff the secret is copied (not shared-and-mutably-rotated).
130 ///
131 /// **Catches:** a regression where `Clone` shares the underlying
132 /// storage via `Arc` without `CoW` semantics — a single rotate would
133 /// then silently corrupt one of the copies.
134 #[test]
135 fn clone_preserves_equality() {
136 let secret = Zeroizing::new(vec![0x11u8; 32]);
137 let public = BlsSigning::public_key(&secret).unwrap();
138 let h1: SignerHandle<BlsSigning> = SignerHandle::from_parts(secret, public);
139 let h2 = h1.clone();
140 let s1 = h1.sign(b"x");
141 let s2 = h2.sign(b"x");
142 assert_eq!(s1.to_bytes(), s2.to_bytes());
143 }
144}