Skip to main content

tempo_consensus_config/
lib.rs

1//! Definitions to read and write a tempo consensus configuration.
2
3#![cfg_attr(not(test), warn(unused_crate_dependencies))]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5
6use std::{
7    fmt::Display,
8    io::{Read as _, Write as _},
9    path::Path,
10};
11
12use commonware_codec::{DecodeExt as _, Encode as _, FixedSize as _, Write as CodecWrite};
13use commonware_cryptography::{
14    Signer,
15    bls12381::primitives::group::Share,
16    ed25519::{PrivateKey, PublicKey},
17};
18use commonware_math::algebra::Random as _;
19use rand_core::CryptoRngCore;
20use secrecy::{ExposeSecret as _, ExposeSecretMut as _, SecretBox, SecretString};
21
22#[cfg(test)]
23mod tests;
24
25pub type SigningKeyPassphrase = SecretString;
26
27pub const MAX_SIGNING_KEY_PASSPHRASE_BYTES: u64 = 1024;
28
29/// Reads a signing-key passphrase from `path`.
30///
31/// The returned boolean reports whether the opened handle is a FIFO.
32pub fn read_secret<P: AsRef<Path>>(path: P) -> std::io::Result<(SigningKeyPassphrase, bool)> {
33    use std::os::unix::fs::FileTypeExt as _;
34
35    let file = std::fs::File::open(path)?;
36    let is_fifo = file.metadata()?.file_type().is_fifo();
37
38    Ok((read_secret_inner(file)?, is_fifo))
39}
40
41fn read_secret_inner<R: std::io::Read>(reader: R) -> std::io::Result<SigningKeyPassphrase> {
42    let mut reader = reader;
43    let mut read_result = Ok(());
44    let mut passphrase = SecretBox::init_with_mut(|buf: &mut String| {
45        buf.reserve_exact((MAX_SIGNING_KEY_PASSPHRASE_BYTES + 1) as usize);
46
47        let mut reader =
48            std::io::BufReader::new(&mut reader).take(MAX_SIGNING_KEY_PASSPHRASE_BYTES + 1);
49        read_result = reader.read_to_string(buf).map(|_| ());
50        if read_result.is_err() {
51            return;
52        }
53
54        if buf.len() as u64 > MAX_SIGNING_KEY_PASSPHRASE_BYTES {
55            read_result = Err(std::io::Error::new(
56                std::io::ErrorKind::InvalidData,
57                format!("passphrase exceeds {MAX_SIGNING_KEY_PASSPHRASE_BYTES} byte limit"),
58            ));
59            return;
60        }
61
62        while matches!(buf.as_bytes().last(), Some(b'\r' | b'\n')) {
63            buf.pop();
64        }
65    });
66
67    read_result?;
68
69    // TODO: `SecretString::from(String)` uses `String::into_boxed_str`, which
70    // can reallocate and leave secret bytes behind in the old allocation.
71    Ok(SecretString::from(std::mem::take(
72        passphrase.expose_secret_mut(),
73    )))
74}
75
76#[derive(Clone, Debug)]
77pub struct SigningKey {
78    inner: PrivateKey,
79}
80
81impl SigningKey {
82    pub fn into_inner(self) -> PrivateKey {
83        self.inner
84    }
85
86    /// Generates a fresh, cryptographically random signing key using `rng`.
87    pub fn random<R: CryptoRngCore>(rng: R) -> Self {
88        Self {
89            inner: PrivateKey::random(rng),
90        }
91    }
92
93    pub fn read_from_file_unencrypted<P: AsRef<Path>>(path: P) -> Result<Self, SigningKeyError> {
94        let hex = std::fs::read_to_string(path).map_err(SigningKeyErrorKind::Read)?;
95        Self::try_from_hex(hex.trim())
96    }
97
98    /// Reads a passphrase-encrypted signing key from `path`.
99    ///
100    /// The file is expected to be an [age](https://age-encryption.org/v1)
101    /// payload produced via passphrase encryption (e.g. `age -p`) whose
102    /// plaintext is the raw encoded ed25519 private key (as produced by
103    /// commonware-codec's `Encode` impl on [`PrivateKey`]).
104    ///
105    /// After decryption the plaintext buffer is zeroized.
106    pub fn read_from_file_encrypted<P: AsRef<Path>>(
107        path: P,
108        passphrase: SecretString,
109    ) -> Result<Self, SigningKeyError> {
110        let file = std::fs::File::open(path).map_err(SigningKeyErrorKind::Read)?;
111        Self::read_encrypted(std::io::BufReader::new(file), passphrase)
112    }
113
114    /// Reads a passphrase-encrypted signing key from an arbitrary
115    /// [`std::io::Read`].
116    pub fn read_encrypted<R: std::io::BufRead>(
117        ciphertext: R,
118        passphrase: SecretString,
119    ) -> Result<Self, SigningKeyError> {
120        let decryptor =
121            age::Decryptor::new_buffered(ciphertext).map_err(SigningKeyErrorKind::Decrypt)?;
122        let identity = age::scrypt::Identity::new(passphrase);
123
124        let mut reader = decryptor
125            .decrypt(std::iter::once(&identity as &dyn age::Identity))
126            .map_err(SigningKeyErrorKind::Decrypt)?;
127
128        let mut io_err: Option<std::io::Error> = None;
129        let plaintext: SecretBox<[u8; PrivateKey::SIZE]> =
130            SecretBox::init_with_mut(|buf: &mut [u8; PrivateKey::SIZE]| {
131                io_err = reader.read_exact(buf).err()
132            });
133        if let Some(err) = io_err {
134            return Err(SigningKeyErrorKind::Read(err).into());
135        }
136
137        let inner = PrivateKey::decode(plaintext.expose_secret().as_ref())
138            .map_err(SigningKeyErrorKind::Parse)?;
139        Ok(Self { inner })
140    }
141
142    /// Writes the signing key to `path` as a passphrase-encrypted age payload.
143    pub fn write_to_file_encrypted<P: AsRef<Path>>(
144        &self,
145        path: P,
146        passphrase: SecretString,
147    ) -> Result<(), SigningKeyError> {
148        let file = std::fs::File::create(path).map_err(SigningKeyErrorKind::Write)?;
149        self.write_encrypted(file, passphrase)
150    }
151
152    /// Writes the signing key to an arbitrary [`std::io::Write`] as a
153    /// passphrase-encrypted age payload. See [`Self::write_to_file_encrypted`].
154    pub fn write_encrypted<W: std::io::Write>(
155        &self,
156        writer: W,
157        passphrase: SecretString,
158    ) -> Result<(), SigningKeyError> {
159        // Serialize the private key bytes directly into a fixed-size,
160        // auto-zeroizing buffer - no transient `Bytes`/`Vec` on the heap.
161        let plaintext: SecretBox<[u8; PrivateKey::SIZE]> =
162            SecretBox::init_with_mut(|buf: &mut [u8; PrivateKey::SIZE]| {
163                let mut tail: &mut [u8] = buf;
164                CodecWrite::write(&self.inner, &mut tail);
165            });
166
167        let mut age_writer = age::Encryptor::with_user_passphrase(passphrase)
168            .wrap_output(writer)
169            .map_err(SigningKeyErrorKind::Write)?;
170        age_writer
171            .write_all(plaintext.expose_secret())
172            .map_err(SigningKeyErrorKind::Write)?;
173        age_writer.finish().map_err(SigningKeyErrorKind::Write)?;
174
175        Ok(())
176    }
177
178    pub fn try_from_hex(hex: &str) -> Result<Self, SigningKeyError> {
179        let bytes = const_hex::decode(hex).map_err(SigningKeyErrorKind::Hex)?;
180        let inner = PrivateKey::decode(&bytes[..]).map_err(SigningKeyErrorKind::Parse)?;
181        Ok(Self { inner })
182    }
183
184    /// Writes the signing key to `writer` as `0x`-prefixed hex of the
185    /// raw encoded ed25519 private key bytes.
186    pub fn to_writer_unencrypted<W: std::io::Write>(
187        &self,
188        mut writer: W,
189    ) -> Result<(), SigningKeyError> {
190        let hex = const_hex::encode_prefixed(self.inner.encode().as_ref());
191        writer
192            .write_all(hex.as_bytes())
193            .map_err(SigningKeyErrorKind::Write)?;
194        Ok(())
195    }
196
197    pub fn public_key(&self) -> PublicKey {
198        self.inner.public_key()
199    }
200}
201
202impl From<PrivateKey> for SigningKey {
203    fn from(inner: PrivateKey) -> Self {
204        Self { inner }
205    }
206}
207
208#[derive(Debug, thiserror::Error)]
209#[error(transparent)]
210pub struct SigningKeyError {
211    #[from]
212    inner: SigningKeyErrorKind,
213}
214
215#[derive(Debug, thiserror::Error)]
216enum SigningKeyErrorKind {
217    #[error("failed decoding file contents as hex-encoded bytes")]
218    Hex(#[source] const_hex::FromHexError),
219    #[error("failed parsing hex-decoded bytes as ed25519 private key")]
220    Parse(#[source] commonware_codec::Error),
221    #[error("failed reading file")]
222    Read(#[source] std::io::Error),
223    #[error("failed writing to file")]
224    Write(#[source] std::io::Error),
225    #[error("failed decrypting age payload (wrong passphrase or malformed file?)")]
226    Decrypt(#[source] age::DecryptError),
227}
228
229#[derive(Clone, Debug, PartialEq, Eq)]
230pub struct SigningShare {
231    inner: Share,
232}
233
234impl SigningShare {
235    pub fn into_inner(self) -> Share {
236        self.inner
237    }
238
239    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, SigningShareError> {
240        let hex = std::fs::read_to_string(path).map_err(SigningShareErrorKind::Read)?;
241        Self::try_from_hex(hex.trim())
242    }
243
244    pub fn try_from_hex(hex: &str) -> Result<Self, SigningShareError> {
245        let bytes = const_hex::decode(hex).map_err(SigningShareErrorKind::Hex)?;
246        let inner = Share::decode(&bytes[..]).map_err(SigningShareErrorKind::Parse)?;
247        Ok(Self { inner })
248    }
249
250    pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), SigningShareError> {
251        std::fs::write(path, self.to_string()).map_err(SigningShareErrorKind::Write)?;
252        Ok(())
253    }
254}
255
256impl Display for SigningShare {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        f.write_str(&const_hex::encode_prefixed(self.inner.encode().as_ref()))
259    }
260}
261
262impl From<Share> for SigningShare {
263    fn from(inner: Share) -> Self {
264        Self { inner }
265    }
266}
267
268#[derive(Debug, thiserror::Error)]
269#[error(transparent)]
270pub struct SigningShareError {
271    #[from]
272    inner: SigningShareErrorKind,
273}
274
275#[derive(Debug, thiserror::Error)]
276enum SigningShareErrorKind {
277    #[error("failed decoding file contents as hex-encoded bytes")]
278    Hex(#[source] const_hex::FromHexError),
279    #[error("failed parsing hex-decoded bytes as bls12381 private share")]
280    Parse(#[source] commonware_codec::Error),
281    #[error("failed reading file")]
282    Read(#[source] std::io::Error),
283    #[error("failed writing to file")]
284    Write(#[source] std::io::Error),
285}