Skip to main content

fips_identity/
local.rs

1//! Local node identity with signing capability.
2
3use secp256k1::{Keypair, PublicKey, SecretKey, XOnlyPublicKey};
4use std::fmt;
5
6use super::auth::{AuthResponse, auth_challenge_digest};
7use super::encoding::{decode_secret, encode_npub};
8use super::{FipsAddress, IdentityError, NodeAddr, sha256};
9
10/// A FIPS node identity consisting of a keypair and derived identifiers.
11///
12/// The identity holds the secp256k1 keypair and provides methods for signing
13/// and verifying protocol messages.
14pub struct Identity {
15    keypair: Keypair,
16    node_addr: NodeAddr,
17    address: FipsAddress,
18}
19
20impl Identity {
21    /// Create a new random identity.
22    pub fn generate() -> Self {
23        let mut secret_bytes = [0u8; 32];
24        rand::Rng::fill_bytes(&mut rand::rng(), &mut secret_bytes);
25        let secret_key =
26            SecretKey::from_slice(&secret_bytes).expect("32 random bytes is a valid secret key");
27        Self::from_secret_key(secret_key)
28    }
29
30    /// Create an identity from an existing keypair.
31    pub fn from_keypair(keypair: Keypair) -> Self {
32        let (pubkey, _parity) = keypair.x_only_public_key();
33        let node_addr = NodeAddr::from_pubkey(&pubkey);
34        let address = FipsAddress::from_node_addr(&node_addr);
35        Self {
36            keypair,
37            node_addr,
38            address,
39        }
40    }
41
42    /// Create an identity from a secret key.
43    pub fn from_secret_key(secret_key: SecretKey) -> Self {
44        let keypair = Keypair::from_secret_key(&super::SECP, &secret_key);
45        Self::from_keypair(keypair)
46    }
47
48    /// Create an identity from secret key bytes.
49    pub fn from_secret_bytes(bytes: &[u8; 32]) -> Result<Self, IdentityError> {
50        let secret_key = SecretKey::from_slice(bytes)?;
51        Ok(Self::from_secret_key(secret_key))
52    }
53
54    /// Create an identity from an nsec string (bech32) or hex-encoded secret.
55    pub fn from_secret_str(s: &str) -> Result<Self, IdentityError> {
56        let secret_key = decode_secret(s)?;
57        Ok(Self::from_secret_key(secret_key))
58    }
59
60    /// Return the underlying keypair.
61    ///
62    /// This is needed for cryptographic operations like Noise handshakes.
63    pub fn keypair(&self) -> Keypair {
64        self.keypair
65    }
66
67    /// Return the x-only public key.
68    pub fn pubkey(&self) -> XOnlyPublicKey {
69        self.keypair.x_only_public_key().0
70    }
71
72    /// Return the full public key (includes parity).
73    pub fn pubkey_full(&self) -> PublicKey {
74        self.keypair.public_key()
75    }
76
77    /// Return the public key as a bech32-encoded npub string (NIP-19).
78    pub fn npub(&self) -> String {
79        encode_npub(&self.pubkey())
80    }
81
82    /// Return the node ID.
83    pub fn node_addr(&self) -> &NodeAddr {
84        &self.node_addr
85    }
86
87    /// Return the FIPS address.
88    pub fn address(&self) -> &FipsAddress {
89        &self.address
90    }
91
92    /// Sign arbitrary data with this identity's secret key.
93    pub fn sign(&self, data: &[u8]) -> secp256k1::schnorr::Signature {
94        let digest = sha256(data);
95        super::SECP.sign_schnorr(&digest, &self.keypair)
96    }
97
98    /// Create an authentication response for a challenge.
99    ///
100    /// The response signs: SHA256("fips-auth-v1" || challenge || timestamp)
101    pub fn sign_challenge(&self, challenge: &[u8; 32], timestamp: u64) -> AuthResponse {
102        let digest = auth_challenge_digest(challenge, timestamp);
103        let signature = super::SECP.sign_schnorr(&digest, &self.keypair);
104        AuthResponse {
105            pubkey: self.pubkey(),
106            timestamp,
107            signature,
108        }
109    }
110}
111
112impl fmt::Debug for Identity {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.debug_struct("Identity")
115            .field("node_addr", &self.node_addr)
116            .field("address", &self.address)
117            .finish_non_exhaustive()
118    }
119}