lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 65de042fbec33cdcf4f51a513232d9dcc3fa25f8
parent c9c069d726ab62b39ae1c15e2a8c17a2120ec8c3
Author: triesap <tyson@radroots.org>
Date:   Wed, 24 Dec 2025 16:15:44 +0000

identity: validate optional public key in identity file


- Add optional public_key field to identity JSON serialization
- Parse public key from npub or hex and reject empty values
- Verify provided public key matches secret key during load
- Add tests for npub parsing and mismatch detection

Diffstat:
Midentity/src/error.rs | 9+++++++++
Midentity/src/identity.rs | 25+++++++++++++++++++++++++
Midentity/tests/identity.rs | 33+++++++++++++++++++++++++++++++++
3 files changed, 67 insertions(+), 0 deletions(-)

diff --git a/identity/src/error.rs b/identity/src/error.rs @@ -1,5 +1,8 @@ use thiserror::Error; +#[cfg(not(feature = "std"))] +use alloc::string::String; + #[cfg(feature = "std")] use radroots_runtime::RuntimeJsonError; #[cfg(feature = "std")] @@ -28,6 +31,12 @@ pub enum IdentityError { #[error("invalid secret key: {0}")] InvalidSecretKey(#[from] nostr::key::Error), + #[error("invalid public key: {0}")] + InvalidPublicKey(String), + + #[error("public key does not match secret key")] + PublicKeyMismatch, + #[error("unsupported identity file format")] InvalidIdentityFormat, diff --git a/identity/src/identity.rs b/identity/src/identity.rs @@ -35,6 +35,8 @@ pub struct RadrootsIdentityProfile { pub struct RadrootsIdentityFile { pub secret_key: String, #[serde(skip_serializing_if = "Option::is_none")] + pub public_key: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] pub identifier: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option<nostr::Event>, @@ -157,6 +159,7 @@ impl RadrootsIdentity { }; RadrootsIdentityFile { secret_key, + public_key: Some(self.public_key_hex()), identifier, metadata, application_handler, @@ -224,6 +227,7 @@ impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity { fn try_from(file: RadrootsIdentityFile) -> Result<Self, Self::Error> { let keys = Keys::parse(&file.secret_key)?; + validate_public_key(&keys, file.public_key.as_deref())?; let profile = RadrootsIdentityProfile { identifier: file.identifier, metadata: file.metadata, @@ -272,6 +276,27 @@ fn parse_identity_bytes(bytes: &[u8]) -> Result<RadrootsIdentity, IdentityError> RadrootsIdentity::from_secret_key_str(trimmed) } +fn validate_public_key(keys: &Keys, public_key: Option<&str>) -> Result<(), IdentityError> { + let Some(public_key) = public_key else { + return Ok(()); + }; + let parsed = parse_public_key(public_key)?; + if parsed != keys.public_key() { + return Err(IdentityError::PublicKeyMismatch); + } + Ok(()) +} + +fn parse_public_key(value: &str) -> Result<nostr::PublicKey, IdentityError> { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(IdentityError::InvalidPublicKey(value.to_string())); + } + nostr::PublicKey::parse(trimmed) + .or_else(|_| nostr::PublicKey::from_hex(trimmed)) + .map_err(|_| IdentityError::InvalidPublicKey(value.to_string())) +} + fn infallible_to_string(value: Result<String, Infallible>) -> String { match value { Ok(value) => value, diff --git a/identity/tests/identity.rs b/identity/tests/identity.rs @@ -76,3 +76,36 @@ fn load_or_generate_missing_allowed_creates_json() { let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); assert_eq!(loaded.public_key(), identity.public_key()); } + +#[test] +fn load_from_json_file_public_key_npub() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys.clone()); + let mut file = identity.to_file(); + file.public_key = Some(identity.public_key_npub()); + let json = serde_json::to_string(&file).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + std::fs::write(&path, json).unwrap(); + + let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); + assert_eq!(loaded.public_key(), keys.public_key()); +} + +#[test] +fn load_from_json_file_public_key_mismatch() { + let keys = nostr::Keys::generate(); + let identity = RadrootsIdentity::new(keys); + let other_keys = nostr::Keys::generate(); + let mut file = identity.to_file(); + file.public_key = Some(other_keys.public_key().to_hex()); + let json = serde_json::to_string(&file).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("identity.json"); + std::fs::write(&path, json).unwrap(); + + let err = RadrootsIdentity::load_from_path_auto(&path).unwrap_err(); + assert!(matches!(err, IdentityError::PublicKeyMismatch)); +}