Skip to main content

confium_net_noise/
keys.rs

1//! Static identities for the Noise_XX handshake.
2
3use sha2::{Digest, Sha256};
4use snow::Builder;
5
6/// A Noise static keypair. The private half never leaves the process
7/// unless the operator provisions it via `to_hex`/`from_hex`.
8#[derive(Clone)]
9pub struct NoiseIdentity {
10    pub(crate) private: Vec<u8>,
11    pub(crate) public: [u8; 32],
12}
13
14impl NoiseIdentity {
15    /// Generate a fresh static keypair from the OS RNG.
16    pub fn generate() -> Self {
17        let keypair = Builder::new(noise_params())
18            .generate_keypair()
19            .expect("snow generates keypairs from the OS RNG");
20        let mut public = [0u8; 32];
21        public.copy_from_slice(&keypair.public);
22        Self {
23            private: keypair.private.clone(),
24            public,
25        }
26    }
27
28    /// The 32-byte static public key.
29    pub fn public(&self) -> &[u8; 32] {
30        &self.public
31    }
32
33    /// Hex encoding of the private key, for provisioning a stable
34    /// identity through configuration.
35    pub fn to_hex(&self) -> String {
36        hex(&self.private)
37    }
38
39    /// Reconstruct an identity from a hex private key. The public
40    /// half is derived with X25519 (snow consumes the private key
41    /// directly; it does not derive the public for us).
42    pub fn from_hex(hex_private: &str) -> Result<Self, String> {
43        let bytes: [u8; 32] = unhex(hex_private)?
44            .try_into()
45            .map_err(|_| "noise private key must be 32 bytes".to_string())?;
46        let secret = x25519_dalek::StaticSecret::from(bytes);
47        let public = x25519_dalek::PublicKey::from(&secret);
48        Ok(Self {
49            private: bytes.to_vec(),
50            public: {
51                let mut p = [0u8; 32];
52                p.copy_from_slice(public.as_bytes());
53                p
54            },
55        })
56    }
57
58    /// SHA-256 fingerprint of the static public key — the value a peer
59    /// pins via the `pinned=` URL parameter.
60    pub fn fingerprint(&self) -> [u8; 32] {
61        fingerprint_of(&self.public)
62    }
63}
64
65impl std::fmt::Debug for NoiseIdentity {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        // Deliberately opaque: never render key material.
68        f.debug_struct("NoiseIdentity")
69            .field("fingerprint", &hex(&fingerprint_of(&self.public)))
70            .finish()
71    }
72}
73
74pub(crate) fn noise_params() -> snow::params::NoiseParams {
75    "Noise_XX_25519_ChaChaPoly_BLAKE2s"
76        .parse()
77        .expect("built-in noise pattern")
78}
79
80pub(crate) fn fingerprint_of(public: &[u8; 32]) -> [u8; 32] {
81    let mut h = Sha256::new();
82    h.update(b"confium-noise-static-v1");
83    h.update(public);
84    h.finalize().into()
85}
86
87pub(crate) fn hex(bytes: &[u8]) -> String {
88    bytes.iter().map(|b| format!("{b:02x}")).collect()
89}
90
91pub(crate) fn unhex(s: &str) -> Result<Vec<u8>, String> {
92    if s.len() % 2 != 0 {
93        return Err("hex string has odd length".into());
94    }
95    (0..s.len() / 2)
96        .map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| format!("bad hex: {e}")))
97        .collect()
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn round_trip_identity() {
106        let id = NoiseIdentity::generate();
107        let restored = NoiseIdentity::from_hex(&id.to_hex()).unwrap();
108        assert_eq!(id.public(), restored.public());
109    }
110
111    #[test]
112    fn bad_hex_rejected() {
113        assert!(NoiseIdentity::from_hex("zz").is_err());
114        assert!(NoiseIdentity::from_hex("abc").is_err());
115    }
116}