Skip to main content

nula_core/key/
keys.rs

1//! BIP-340 secp256k1 keypair convenient for signing.
2//!
3//! [`Keys`] holds a [`SecretKey`] alongside a cached
4//! [`secp256k1::Keypair`]. The cache lets us sign Schnorr messages without
5//! recomputing the public key on every call — important on the hot path of
6//! event creation.
7
8use std::fmt;
9
10use secp256k1::SECP256K1;
11use secp256k1::schnorr::Signature;
12
13use super::{PublicKey, SecretKey, SecretKeyError};
14
15/// Length of a Schnorr signature in bytes (BIP-340).
16pub const SIGNATURE_SIZE: usize = 64;
17
18/// secp256k1 BIP-340 keypair.
19///
20/// Construct from a [`SecretKey`] (`Keys::from_secret_key`) or generate a fresh
21/// pair (`Keys::generate`). The public key is computed eagerly so signing has
22/// O(1) overhead.
23///
24/// # Example
25///
26/// ```
27/// use nula_core::Keys;
28///
29/// let keys = Keys::generate().unwrap();
30/// let pk_hex = keys.public_key().to_hex();
31/// assert_eq!(pk_hex.len(), 64);
32/// ```
33#[derive(Clone)]
34pub struct Keys {
35    secret_key: SecretKey,
36    public_key: PublicKey,
37    keypair: secp256k1::Keypair,
38}
39
40impl Keys {
41    /// Construct a [`Keys`] from a [`SecretKey`].
42    #[must_use]
43    pub fn from_secret_key(secret_key: SecretKey) -> Self {
44        let keypair = secp256k1::Keypair::from_secret_key(SECP256K1, secret_key.as_inner());
45        let (xonly, _parity) = keypair.x_only_public_key();
46        Self {
47            secret_key,
48            public_key: PublicKey::from(xonly),
49            keypair,
50        }
51    }
52
53    /// Generate a fresh [`Keys`] pair using the operating system's entropy.
54    ///
55    /// # Errors
56    ///
57    /// Returns [`SecretKeyError::Rng`] if the OS RNG fails.
58    pub fn generate() -> Result<Self, SecretKeyError> {
59        let secret_key = SecretKey::generate()?;
60        Ok(Self::from_secret_key(secret_key))
61    }
62
63    /// Parse [`Keys`] from a 64-char lowercase hex secret key.
64    ///
65    /// # Errors
66    ///
67    /// See [`SecretKeyError`].
68    pub fn parse<S>(input: S) -> Result<Self, SecretKeyError>
69    where
70        S: AsRef<str>,
71    {
72        let secret_key = SecretKey::parse(input)?;
73        Ok(Self::from_secret_key(secret_key))
74    }
75
76    /// Borrow the secret key.
77    #[must_use]
78    pub const fn secret_key(&self) -> &SecretKey {
79        &self.secret_key
80    }
81
82    /// Borrow the public key.
83    #[must_use]
84    pub const fn public_key(&self) -> &PublicKey {
85        &self.public_key
86    }
87
88    /// Borrow the inner [`secp256k1::Keypair`].
89    ///
90    /// Use this only at the boundary with the cryptography backend.
91    #[must_use]
92    pub const fn as_inner(&self) -> &secp256k1::Keypair {
93        &self.keypair
94    }
95
96    /// Sign an arbitrary message digest with BIP-340 Schnorr.
97    ///
98    /// Callers are responsible for hashing application data — for Nostr
99    /// events, the digest is the SHA-256 of the canonical serialization
100    /// described in NIP-01.
101    #[must_use]
102    pub fn sign_schnorr(&self, message: &[u8; 32]) -> Signature {
103        self.keypair.sign_schnorr(message)
104    }
105}
106
107impl fmt::Debug for Keys {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.debug_struct("Keys")
110            .field("public_key", &self.public_key)
111            .field("secret_key", &"<redacted>")
112            .finish_non_exhaustive()
113    }
114}
115
116impl Drop for Keys {
117    fn drop(&mut self) {
118        // Erase the cached keypair's private half. The wrapped `SecretKey`
119        // is erased independently by its own `Drop` impl, so both copies of
120        // the secret material are best-effort zeroized when `Keys` falls
121        // out of scope.
122        self.keypair.non_secure_erase();
123    }
124}
125
126impl PartialEq for Keys {
127    fn eq(&self, other: &Self) -> bool {
128        self.secret_key == other.secret_key
129    }
130}
131
132impl Eq for Keys {}
133
134impl From<SecretKey> for Keys {
135    fn from(secret_key: SecretKey) -> Self {
136        Self::from_secret_key(secret_key)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use hex_literal::hex;
143
144    use super::*;
145
146    /// BIP-340 test vector 0.
147    const SECRET_HEX: &str = "0000000000000000000000000000000000000000000000000000000000000003";
148    const EXPECTED_PUBKEY: [u8; 32] =
149        hex!("F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9");
150
151    #[test]
152    fn derives_expected_public_key() {
153        let keys = Keys::parse(SECRET_HEX).unwrap();
154        assert_eq!(keys.public_key().to_byte_array(), EXPECTED_PUBKEY);
155    }
156
157    #[test]
158    fn generate_distinct() {
159        let lhs = Keys::generate().unwrap();
160        let rhs = Keys::generate().unwrap();
161        assert_ne!(lhs, rhs);
162    }
163
164    #[test]
165    fn signs_and_verifies() {
166        let keys = Keys::parse(SECRET_HEX).unwrap();
167        let message = hex!("0202020202020202020202020202020202020202020202020202020202020202");
168        let sig = keys.sign_schnorr(&message);
169        assert!(
170            SECP256K1
171                .verify_schnorr(&sig, &message, keys.public_key().as_inner())
172                .is_ok()
173        );
174    }
175
176    #[test]
177    fn debug_redacts_secret() {
178        let keys = Keys::parse(SECRET_HEX).unwrap();
179        let dbg = format!("{keys:?}");
180        assert!(dbg.contains("redacted"));
181        assert!(!dbg.contains(SECRET_HEX));
182    }
183
184    #[test]
185    fn equality_compares_secret_only() {
186        let lhs = Keys::parse(SECRET_HEX).unwrap();
187        let rhs = Keys::parse(SECRET_HEX).unwrap();
188        assert_eq!(lhs, rhs);
189    }
190}