lib

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

key.rs (12521B)


      1 //! Canonical public-key and identity identifier value types.
      2 //!
      3 //! [`PublicKey`] validates a 32-byte x-only secp256k1 public key. [`IdentityId`]
      4 //! is a distinct Rust type derived from the same canonical bytes. Both values
      5 //! parse exact-width hexadecimal text, preserve fixed-width binary form, and
      6 //! emit lowercase hexadecimal text without exposing secret-key or Nostr
      7 //! bech32 behavior.
      8 
      9 use crate::Error;
     10 
     11 pub(crate) const IDENTIFIER_BYTE_LENGTH: usize = 32;
     12 pub(crate) const IDENTIFIER_HEX_LENGTH: usize = IDENTIFIER_BYTE_LENGTH * 2;
     13 
     14 const HEX_ALPHABET: &[u8; 16] = b"0123456789abcdef";
     15 
     16 pub(crate) struct EncodedHex([u8; IDENTIFIER_HEX_LENGTH]);
     17 
     18 impl EncodedHex {
     19     pub(crate) fn new(bytes: &[u8; IDENTIFIER_BYTE_LENGTH]) -> Self {
     20         let mut encoded = [0; IDENTIFIER_HEX_LENGTH];
     21         for (index, byte) in bytes.iter().copied().enumerate() {
     22             encoded[index * 2] = HEX_ALPHABET[usize::from(byte >> 4)];
     23             encoded[index * 2 + 1] = HEX_ALPHABET[usize::from(byte & 0x0f)];
     24         }
     25         Self(encoded)
     26     }
     27 
     28     pub(crate) fn as_str(&self) -> &str {
     29         core::str::from_utf8(&self.0).expect("the hexadecimal alphabet is valid UTF-8")
     30     }
     31 }
     32 
     33 fn decode_nibble(byte: u8, index: usize) -> Result<u8, Error> {
     34     match byte {
     35         b'0'..=b'9' => Ok(byte - b'0'),
     36         b'a'..=b'f' => Ok(byte - b'a' + 10),
     37         b'A'..=b'F' => Ok(byte - b'A' + 10),
     38         _ => Err(Error::InvalidHexCharacter { index }),
     39     }
     40 }
     41 
     42 pub(crate) fn parse_hex(value: &str) -> Result<[u8; IDENTIFIER_BYTE_LENGTH], Error> {
     43     let encoded = value.as_bytes();
     44     if encoded.len() != IDENTIFIER_HEX_LENGTH {
     45         return Err(Error::InvalidHexLength {
     46             expected: IDENTIFIER_HEX_LENGTH,
     47             actual: encoded.len(),
     48         });
     49     }
     50 
     51     let mut bytes = [0; IDENTIFIER_BYTE_LENGTH];
     52     for (index, output) in bytes.iter_mut().enumerate() {
     53         let high_index = index * 2;
     54         let high = decode_nibble(encoded[high_index], high_index)?;
     55         let low = decode_nibble(encoded[high_index + 1], high_index + 1)?;
     56         *output = (high << 4) | low;
     57     }
     58     Ok(bytes)
     59 }
     60 
     61 pub(crate) fn validate_public_key_bytes(bytes: &[u8; IDENTIFIER_BYTE_LENGTH]) -> Result<(), Error> {
     62     let mut compressed = [0; IDENTIFIER_BYTE_LENGTH + 1];
     63     compressed[0] = 0x02;
     64     compressed[1..].copy_from_slice(bytes);
     65     k256::PublicKey::from_sec1_bytes(&compressed)
     66         .map(|_| ())
     67         .map_err(|_| Error::InvalidPublicKeyBytes)
     68 }
     69 
     70 macro_rules! define_identifier {
     71     ($(#[$meta:meta])* $visibility:vis struct $name:ident;) => {
     72         $(#[$meta])*
     73         #[repr(transparent)]
     74         #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     75         $visibility struct $name([u8; $crate::key::IDENTIFIER_BYTE_LENGTH]);
     76 
     77         impl $name {
     78             /// The canonical binary representation length.
     79             pub const BYTE_LENGTH: usize = $crate::key::IDENTIFIER_BYTE_LENGTH;
     80 
     81             /// The canonical hexadecimal representation length.
     82             pub const HEX_LENGTH: usize = $crate::key::IDENTIFIER_HEX_LENGTH;
     83 
     84             /// Constructs the value from its canonical fixed-width bytes.
     85             pub fn from_bytes(bytes: [u8; Self::BYTE_LENGTH]) -> Result<Self, $crate::Error> {
     86                 $crate::key::validate_public_key_bytes(&bytes)?;
     87                 Ok(Self::from_validated_bytes(bytes))
     88             }
     89 
     90             pub(crate) const fn from_validated_bytes(
     91                 bytes: [u8; Self::BYTE_LENGTH],
     92             ) -> Self {
     93                 Self(bytes)
     94             }
     95 
     96             /// Parses an exact-width byte slice.
     97             pub fn from_slice(bytes: &[u8]) -> Result<Self, $crate::Error> {
     98                 let bytes: [u8; Self::BYTE_LENGTH] = bytes.try_into().map_err(|_| {
     99                     $crate::Error::InvalidByteLength {
    100                         expected: Self::BYTE_LENGTH,
    101                         actual: bytes.len(),
    102                     }
    103                 })?;
    104                 Self::from_bytes(bytes)
    105             }
    106 
    107             /// Parses a 64-character hexadecimal representation.
    108             pub fn from_hex(value: &str) -> Result<Self, $crate::Error> {
    109                 Self::from_bytes($crate::key::parse_hex(value)?)
    110             }
    111 
    112             /// Borrows the canonical fixed-width bytes.
    113             #[must_use]
    114             pub const fn as_bytes(&self) -> &[u8; Self::BYTE_LENGTH] {
    115                 &self.0
    116             }
    117 
    118             /// Returns the canonical fixed-width bytes.
    119             #[must_use]
    120             pub const fn into_bytes(self) -> [u8; Self::BYTE_LENGTH] {
    121                 self.0
    122             }
    123 
    124             /// Encodes the value as canonical lowercase hexadecimal text.
    125             #[must_use]
    126             pub fn to_hex(self) -> alloc::string::String {
    127                 alloc::string::String::from(
    128                     $crate::key::EncodedHex::new(&self.0).as_str(),
    129                 )
    130             }
    131         }
    132 
    133         impl core::fmt::Debug for $name {
    134             fn fmt(
    135                 &self,
    136                 formatter: &mut core::fmt::Formatter<'_>,
    137             ) -> core::fmt::Result {
    138                 write!(formatter, "{}(\"{}\")", stringify!($name), self)
    139             }
    140         }
    141 
    142         impl core::fmt::Display for $name {
    143             fn fmt(
    144                 &self,
    145                 formatter: &mut core::fmt::Formatter<'_>,
    146             ) -> core::fmt::Result {
    147                 formatter.write_str($crate::key::EncodedHex::new(&self.0).as_str())
    148             }
    149         }
    150 
    151         impl core::str::FromStr for $name {
    152             type Err = $crate::Error;
    153 
    154             fn from_str(value: &str) -> Result<Self, Self::Err> {
    155                 Self::from_hex(value)
    156             }
    157         }
    158 
    159         impl TryFrom<&str> for $name {
    160             type Error = $crate::Error;
    161 
    162             fn try_from(value: &str) -> Result<Self, Self::Error> {
    163                 Self::from_hex(value)
    164             }
    165         }
    166 
    167         impl TryFrom<alloc::string::String> for $name {
    168             type Error = $crate::Error;
    169 
    170             fn try_from(value: alloc::string::String) -> Result<Self, Self::Error> {
    171                 Self::from_hex(&value)
    172             }
    173         }
    174 
    175         impl TryFrom<&[u8]> for $name {
    176             type Error = $crate::Error;
    177 
    178             fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
    179                 Self::from_slice(value)
    180             }
    181         }
    182 
    183         impl TryFrom<[u8; $crate::key::IDENTIFIER_BYTE_LENGTH]> for $name {
    184             type Error = $crate::Error;
    185 
    186             fn try_from(
    187                 value: [u8; $crate::key::IDENTIFIER_BYTE_LENGTH],
    188             ) -> Result<Self, Self::Error> {
    189                 Self::from_bytes(value)
    190             }
    191         }
    192 
    193         impl AsRef<[u8]> for $name {
    194             fn as_ref(&self) -> &[u8] {
    195                 self.as_bytes()
    196             }
    197         }
    198 
    199         #[cfg(feature = "serde")]
    200         impl serde::Serialize for $name {
    201             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    202             where
    203                 S: serde::Serializer,
    204             {
    205                 serializer.serialize_str($crate::key::EncodedHex::new(&self.0).as_str())
    206             }
    207         }
    208 
    209         #[cfg(feature = "serde")]
    210         impl<'de> serde::Deserialize<'de> for $name {
    211             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    212             where
    213                 D: serde::Deserializer<'de>,
    214             {
    215                 struct IdentifierVisitor;
    216 
    217                 impl serde::de::Visitor<'_> for IdentifierVisitor {
    218                     type Value = $name;
    219 
    220                     fn expecting(
    221                         &self,
    222                         formatter: &mut core::fmt::Formatter<'_>,
    223                     ) -> core::fmt::Result {
    224                         formatter.write_str(concat!(
    225                             "a 64-character hexadecimal ",
    226                             stringify!($name)
    227                         ))
    228                     }
    229 
    230                     fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    231                     where
    232                         E: serde::de::Error,
    233                     {
    234                         $name::from_hex(value).map_err(E::custom)
    235                     }
    236                 }
    237 
    238                 deserializer.deserialize_str(IdentifierVisitor)
    239             }
    240         }
    241     };
    242 }
    243 
    244 pub(crate) use define_identifier;
    245 
    246 define_identifier! {
    247     /// A canonical 32-byte public key.
    248     ///
    249     /// The key is an explicit byte value and intentionally does not dereference
    250     /// to text:
    251     ///
    252     /// ```compile_fail
    253     /// use radroots_identity::PublicKey;
    254     ///
    255     /// fn accepts_text(_: &str) {}
    256     /// let key = PublicKey::from_hex(
    257     ///     "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df",
    258     /// ).unwrap();
    259     /// accepts_text(&key);
    260     /// ```
    261     pub struct PublicKey;
    262 }
    263 
    264 define_identifier! {
    265     /// A canonical public identity identifier.
    266     pub struct IdentityId;
    267 }
    268 
    269 impl IdentityId {
    270     /// Derives the identity identifier from its canonical public key.
    271     #[must_use]
    272     pub const fn from_public_key(public_key: PublicKey) -> Self {
    273         Self::from_validated_bytes(public_key.into_bytes())
    274     }
    275 }
    276 
    277 impl From<PublicKey> for IdentityId {
    278     fn from(value: PublicKey) -> Self {
    279         Self::from_public_key(value)
    280     }
    281 }
    282 
    283 #[cfg(test)]
    284 mod tests {
    285     #[cfg(feature = "serde")]
    286     use alloc::format;
    287     use alloc::string::{String, ToString};
    288     use core::str::FromStr;
    289 
    290     use super::*;
    291 
    292     const ALICE: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
    293     const BOB: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af";
    294 
    295     #[test]
    296     fn public_keys_validate_and_canonicalize_hex() {
    297         let uppercase = ALICE.to_ascii_uppercase();
    298         let public_key = PublicKey::from_hex(&uppercase).expect("valid fixture public key");
    299 
    300         assert_eq!(public_key.to_hex(), ALICE);
    301         assert_eq!(public_key.to_string(), ALICE);
    302         assert_eq!(PublicKey::from_str(ALICE).unwrap(), public_key);
    303         assert_eq!(PublicKey::try_from(ALICE).unwrap(), public_key);
    304     }
    305 
    306     #[test]
    307     fn public_keys_reject_invalid_encodings_and_curve_points() {
    308         assert!(matches!(
    309             PublicKey::from_hex("00"),
    310             Err(Error::InvalidHexLength {
    311                 expected: PublicKey::HEX_LENGTH,
    312                 actual: 2,
    313             })
    314         ));
    315 
    316         let mut invalid_hex = ALICE.as_bytes().to_vec();
    317         invalid_hex[17] = b'g';
    318         let invalid_hex = String::from_utf8(invalid_hex).expect("ASCII test input");
    319         assert!(matches!(
    320             PublicKey::from_hex(&invalid_hex),
    321             Err(Error::InvalidHexCharacter { index: 17 })
    322         ));
    323         assert!(matches!(
    324             PublicKey::from_bytes([0; PublicKey::BYTE_LENGTH]),
    325             Err(Error::InvalidPublicKeyBytes)
    326         ));
    327         assert!(matches!(
    328             PublicKey::from_slice(&[0; PublicKey::BYTE_LENGTH - 1]),
    329             Err(Error::InvalidByteLength {
    330                 expected: PublicKey::BYTE_LENGTH,
    331                 actual,
    332             }) if actual == PublicKey::BYTE_LENGTH - 1
    333         ));
    334     }
    335 
    336     #[test]
    337     fn canonical_bytes_round_trip_and_order() {
    338         let alice = PublicKey::from_hex(ALICE).expect("alice fixture");
    339         let bob = PublicKey::from_hex(BOB).expect("bob fixture");
    340 
    341         assert_eq!(PublicKey::from_bytes(alice.into_bytes()).unwrap(), alice);
    342         assert_eq!(PublicKey::try_from(alice.as_ref()).unwrap(), alice);
    343         assert!(alice < bob);
    344     }
    345 
    346     #[test]
    347     fn identity_ids_are_distinct_key_derived_values() {
    348         let public_key = PublicKey::from_hex(ALICE).expect("valid fixture public key");
    349         let identity_id = IdentityId::from(public_key);
    350 
    351         assert_eq!(identity_id.to_hex(), ALICE);
    352         assert_eq!(identity_id.as_bytes(), public_key.as_bytes());
    353         assert_eq!(IdentityId::from_hex(ALICE).unwrap(), identity_id);
    354     }
    355 
    356     #[cfg(feature = "serde")]
    357     #[test]
    358     fn key_values_serde_as_validated_canonical_hex() {
    359         let public_key = PublicKey::from_hex(ALICE).expect("valid fixture public key");
    360         let encoded = serde_json::to_string(&public_key).expect("serialize public key");
    361 
    362         assert_eq!(encoded, format!("\"{ALICE}\""));
    363         assert_eq!(
    364             serde_json::from_str::<PublicKey>(&encoded).expect("deserialize public key"),
    365             public_key
    366         );
    367         assert!(serde_json::from_str::<IdentityId>("\"invalid\"").is_err());
    368     }
    369 }